Repository navigation
Conversation
…AvroRowWriter.tell
|
/gemini review |
There was a problem hiding this comment.
📋 Review Summary
This PR introduces three targeted performance optimizations in bigquery_tools.py: avoiding per-row Avro block flushes in AvroRowWriter.tell(), fast-pathing JSON-safe scalar rows in BigQueryWrapper.insert_rows() and RowAsDictJsonCoder.decode(), and precompiling regular expressions for _split_table_spec(). The changes are well-reasoned and significantly improve throughput on hot paths.
🔍 General Feedback
- In
BigQueryWrapper.insert_rows, the fast-path forintvalues should guard against integers exceeding 64-bit bounds, whichorjsonpreviously routed todefault_encoder. - Adding unit tests for
AvroRowWriter.tell()and theinsert_rowsfast-path/fallback cases inbigquery_tools_test.pywould help prevent future regressions.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
| rows = [ | ||
| r if ( | ||
| type(r) is dict and | ||
| all(type(v) in _json_safe for v in r.values())) else |
There was a problem hiding this comment.
🟡 Medium: When orjson is installed (the default for fast_json_dumps), integers outside the 64-bit range invoke default_encoder (or raise TypeError), whereas bypassing fast_json_dumps passes arbitrary-precision Python int objects directly to gcp_bigquery.Client.insert_rows_json (which uses standard json.dumps and serializes >64-bit unquoted integers in the JSON payload). Additionally, if r has non-str keys, json.dumps / orjson.dumps behaves differently than passing r through unchanged. Consider verifying type(k) is str as well, or ensuring keys are strings:
all(type(k) is str and type(v) in _json_safe for k, v in r.items())| all(type(v) in _json_safe for v in r.values())) else | |
| all(type(k) is str and type(v) in _json_safe for k, v in r.items())) else |
| # Include both bytes already flushed to the underlying stream and bytes | ||
| # currently buffered in the fastavro Writer, avoiding a costly block flush | ||
| # (and 16-byte sync marker) after every single row. | ||
| if hasattr(self._avro_writer, 'io'): |
There was a problem hiding this comment.
🟢 Low: fastavro.write.Writer in both the Cython (_write_py.pyx) and pure-Python (_write_py.py) implementations exposes .io (a MemoryIO or BytesIO buffer). However, AvroRowWriter.tell() previously had no direct unit test verifying that multiple writes stay buffered within a single block rather than flushing after every tell() call. Consider adding a test in TestAvroRowWriter that calls tell() between write() calls and verifies tell() increases monotonically without forcing separate Avro blocks.
Description
Optimizes three hot paths in
sdks/python/apache_beam/io/gcp/bigquery_tools.py:AvroRowWriter.tell():FILE_LOADSwithtemp_file_format=AVRO, the file writer callsAvroRowWriter.tell()after every row to check shard rollover limits. Previously,AvroRowWriter.tell()calledself._avro_writer.flush()beforeself._file_handle.tell(), forcingfastavroto close, compress, and write a 1-row Avro block (plus a 16-byte sync marker) on every single record.self._file_handle.tell() + self._avro_writer.io.tell()includes buffered bytes without flushing the active Avro block (16.05x fasterAvroRowWriterthroughput and 12.2x smaller output files on 50,000 rows).BigQueryWrapper.insert_rowsandRowAsDictJsonCoder.decode:fast_json_loads(fast_json_dumps(r))round-trip inBigQueryWrapper.insert_rowswhen a rowdictcontains only JSON-safe scalar values (str,int,bool,None) (11.61x faster row preparation), and passesbytesdirectly tojson.loads()inRowAsDictJsonCoder.decode._split_table_spec:_COMPILED_TABLE_SPEC_RE,_COMPILED_PROJECT_RE, and_COMPILED_PROJECT_SEGMENT_REto module initialization (2.34x faster_split_table_spec).Testing
pytest sdks/python/apache_beam/io/gcp/bigquery_tools_test.py.