GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — perf: pause the GC while rows are built (optional, stacked on #1039) by vuanhphung · Pull Request #1043 · databricks/databricks-sql-python

Conversation

@vuanhphung

@vuanhphung vuanhphung commented Nov 12, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

Optional, stacked on #1039. The base is perf/fetchall-rows, and this diff only adds a GC pause while rows are built. #1039 is safe and faster than main on its own, so this can be merged after it or not at all. I'll retarget to main after #1039 merges.

What type of PR is this?

  • Other (performance)

Description

After #1039, about a third of fetchall()'s conversion time goes to Python's cyclic garbage collector. Each row adds two GC-tracked objects (the Row and its __dict__), and the full collections triggered along the way rescan every row built so far. For 5M mixed-type rows under Python 3.11, #1039 runs 13,046 young, 1,185 middle and 17 full collections. A non-GC alternative isn't available without changing Row's shape, because the __dict__ holding __fields__ is the second tracked object, and removing it is an API decision.

This change pauses the cyclic GC while each batch of up to 100,000 rows is built. GC state is process-wide, so the pause has these guards:

  • Only conversions of at least 10,000 rows pause, so fetchone()-sized conversions never touch the GC.
  • The pause is shared by threads through a lock-protected counter. The first batch disables the GC only if it is enabled. The last batch to finish re-enables it and runs gc.collect(0), so other threads' cyclic garbage is still collected between batches even when conversions overlap.
  • Batches stop joining a pause once it is 0.5 s old. The pause ends when the batches already in it finish.
  • A GC that is already disabled when a batch starts stays disabled. If other code disables the GC while a pause is running, the end of the pause re-enables it, because the gc module gives no way to tell who disabled it.
  • The pause is exception-safe, including an interrupt that arrives while a batch is joining it. A fork child resets it, since pauses held by the parent's other threads never end there.

How is this tested?

  • Unit tests
  • E2E Tests
  • Manually

The unit tests cover per-batch pausing, small conversions, a caller-disabled GC, an exception mid-batch, overlapping pauses, the 0.5 s limit, the fork reset (including a real os.fork()), and four threads converting at once. They pass on Python 3.9–3.14 with pandas 1.5/2.2/3.1.

The table below is for the 5M-row result from #1039: _convert_arrow_table alone, a fresh process per run, main, #1039 and this PR interleaved, median of 5. "+ later GC" adds a phase after the conversion that keeps allocating. That phase counts the one full collection over the rows that now happens afterwards, where #1039 instead runs 13–17 of them during the conversion. Peak RSS is the same as #1039's.

Python conversion: main / #1039 / this PR + later GC: main / #1039 / this PR
3.9 25.9 / 23.7 / 17.8 s 27.5 / 25.4 / 20.7 s
3.10 26.2 / 24.1 / 18.3 s 27.8 / 25.7 / 21.0 s
3.11 24.4 / 22.7 / 14.7 s 26.6 / 25.0 / 18.8 s
3.12 26.5 / 24.5 / 17.4 s 28.3 / 26.3 / 20.8 s
3.13 23.7 / 21.2 / 14.9 s 26.1 / 23.7 / 19.0 s
3.14 21.3 / 19.6 / 13.7 s 23.5 / 21.9 / 17.7 s

To check concurrency, four threads converted 1M-row results at once for 90 s while a fifth thread created about 10 MB/s of cyclic garbage:

Python per conversion: #1039 / this PR longest GC-off window most uncollected garbage: #1039 / this PR
3.11 19.0 / 14.2 s 1.8 s 1.6 / 2.9 MB
3.13 18.1 / 13.8 s 2.6 s 2.0 / 3.4 MB
3.14 16.8 / 13.9 s 2.4 s 1.3 / 3.1 MB

The GC was enabled at the end of every run. The longest window exceeds 0.5 s because a thread holding the pause can wait for the GIL while other threads run to_pandas(). By comparison, disabling the GC for a whole conversion gave windows of 22–26 s and 23–27 MB of uncollected garbage.

This pull request and its description were written by Isaac.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 18:13 — with GitHub Actions Failure
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:13 — with GitHub Actions Inactive
@vuanhphung vuanhphung mentioned this pull request Nov 12, 2026
perf: convert large Arrow results to rows in slices #1039
Open
4 tasks done
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good. Building rows with tuple.__new__(Row, v) plus setting __fields__ matches what _create_row does, and the tests check the new rows thoroughly against rows from the factory. One low concern: the shared GC pause re-enables the GC unconditionally, so a concurrent gc.disable() by other code during a pause is undone.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/databricks/sql/result_set.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 18:17 Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:33 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:33 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:33 — with GitHub Actions Inactive
@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 18:33 — with GitHub Actions Error
@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 18:36 Inactive
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good, with one low test-coverage gap. Building rows with tuple.__new__(Row, v) plus the __fields__ assignment produces the same objects as Row.__call__/_create_row, with a fallback that keeps the too-many-values error. The shared GC pause handles exceptions, overlapping threads, a GC the caller already disabled, and the 0.5 s cap correctly. Nit: the _disable_pandas path still builds rows with ResultRow(*...) and gets no speedup, so the CHANGELOG line could say the speedup applies to the pandas conversion path.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread tests/unit/test_pandas_compatibility.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 18:37 Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:54 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:54 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 18:54 — with GitHub Actions Inactive
@vuanhphung

vuanhphung commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review-body nit: agreed. The _disable_pandas=True path is unchanged and gets no speedup, so in 3a03036 the CHANGELOG line now says "on the default (pandas) conversion path".

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 18:54 — with GitHub Actions Error
@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 18:56 Inactive
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues identified by the review bot.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@vuanhphung
vuanhphung force-pushed the perf/fetchall-rows-construct branch from 3a03036 to 5721937 Compare November 12, 2026 21:47
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 21:48 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 21:48 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 21:48 — with GitHub Actions Inactive
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues identified by the review bot.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@vuanhphung vuanhphung changed the title perf: build result rows directly and pause the GC per batch (stacked on #1039) perf: pause the GC while rows are built (optional, stacked on #1039) Nov 12, 2026
@vuanhphung Isaac
perf: pause the cyclic GC while each batch of rows is built
056f35a
Every Row and its __dict__ is tracked by the cyclic GC, whose collections
rescan all rows built so far: about a third of fetchall() conversion time
for large results. Pause the GC while each batch of up to 100,000 rows is
built. The pause is shared by threads, batches stop joining it once it is
0.5 s old, it never re-enables a GC that was disabled when a batch
started, it ends with a young-generation collection, and a fork child
resets it.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>
@vuanhphung
vuanhphung force-pushed the perf/fetchall-rows-construct branch from 5721937 to 056f35a Compare November 12, 2026 22:06
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 22:07 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 22:07 — with GitHub Actions Inactive
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 22:07 — with GitHub Actions Inactive
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good: one low robustness concern in _GCPause.__enter__. The counter, the 0.5 s join limit, the fork reset and the exception safety all hold up, and the tests cover each guard. Nit: the CHANGELOG says the GC "is re-enabled between batches", but when conversions overlap it is only re-enabled when the last batch in a shared pause finishes.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/databricks/sql/result_set.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 22:10 Inactive
@vuanhphung Isaac
fix: undo a half-joined GC pause when __enter__ is interrupted
ef58abe
An async exception (e.g. KeyboardInterrupt) arriving after a batch joined
the pause but before __enter__ returned left the pause count at 1, and
the GC disabled, for the rest of the process, since __exit__ never runs
for a failed __enter__. Roll the join back in that case. The CHANGELOG
now says the GC is re-enabled as soon as no batch is being built.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 22:25 — with GitHub Actions Inactive
@vuanhphung
vuanhphung deployed to azure-prod November 12, 2026 22:25 — with GitHub Actions Active
@vuanhphung
vuanhphung temporarily deployed to azure-prod November 12, 2026 22:25 — with GitHub Actions Inactive
@vuanhphung

vuanhphung commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review-body nit: agreed. When conversions overlap, the GC is only re-enabled when the last batch in a shared pause finishes. In ef58abe the CHANGELOG now says it "is re-enabled as soon as no batch is being built, so between batches unless conversions overlap".

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 22:26 — with GitHub Actions Error
@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 22:27 Inactive
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 2 Low

The pause logic looks correct, with 2 low concerns. A rare interrupt between the counter update and the _joined flag can leave the GC disabled for the whole process, and the CHANGELOG doesn't mention that the pause can re-enable a GC the application disabled while a pause was running.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/databricks/sql/result_set.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread CHANGELOG.md Resolved

Uh oh!

There was an error while loading. Please reload this page.

@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 22:28 Inactive
@peco-review-bot
peco-review-bot Bot temporarily deployed to azure-prod November 12, 2026 22:28 Inactive
@vuanhphung Isaac
fix: clear the GC pause flag inside the lock; document the override
f1d4061
__exit__ cleared _joined before taking the lock, so an interrupt at the
lock acquisition could skip the decrement. Clear it inside the lock, with
no call between it and the decrement (as already in __enter__). The
CHANGELOG now says that a gc.disable() made while a batch is being built
is undone when the pause ends, while a GC disabled before fetchall() is
left alone.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>
@vuanhphung
vuanhphung requested a deployment to azure-prod November 12, 2026 22:44 — with GitHub Actions In progress
@vuanhphung
vuanhphung deployed to azure-prod November 12, 2026 22:44 — with GitHub Actions Active
@vuanhphung
vuanhphung deployed to azure-prod November 12, 2026 22:44 — with GitHub Actions Active
@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 22:44 — with GitHub Actions Error
@vuanhphung
vuanhphung had a problem deploying to azure-prod November 12, 2026 22:44 — with GitHub Actions Error
@peco-review-bot
peco-review-bot Bot had a problem deploying to azure-prod November 12, 2026 22:45 Error
@peco-review-bot
peco-review-bot Bot deployed to azure-prod November 12, 2026 22:46 Active
peco-review-bot[bot]
peco-review-bot Bot approved these changes Nov 12, 2026
View reviewed changes

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good overall, with one low concern. If an interrupt arrives at the end of a pause on Python 3.9, the GC can stay disabled permanently. Otherwise the shared-counter design, the 0.5 s join limit, the fork reset and the exception paths are sound and well tested.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/databricks/sql/result_set.py
self._joined = False
_GCPause.active -= 1
if _GCPause.active:
return

@peco-review-bot peco-review-bot Bot Nov 12, 2026

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The comment says no async exception can split these statements, and the code depends on that. On Python 3.9, which this PR supports, that isn't guaranteed. With computed gotos, 3.9's DISPATCH() checks eval_breaker after almost every opcode, so a pending SIGINT can raise KeyboardInterrupt between any two of these bytecodes. Python 3.10 narrowed those checks to backward jumps and calls.

The risky window is between _GCPause.active -= 1 reaching 0 and gc.enable(). If the interrupt lands there:

  • active is 0 and the GC stays disabled.
  • __exit__ has no try/finally, so nothing re-enables it.
  • Every later __enter__ sees active == 0 and not gc.isenabled() and refuses to pause, so nothing ever turns the GC back on.

The result is a process with the GC off permanently. The window is tiny, but Ctrl-C during a long fetchall() in the main thread is exactly when it could happen, and the damage is silent and process-wide. test_interrupt_while_ending_a_pause_keeps_state_consistent only injects the interrupt after gc.enable() runs, so it doesn't cover this ordering.

A more robust order is to re-enable before giving up the last slot, e.g. if _GCPause.active == 1: gc.enable() and then _GCPause.active -= 1. An interrupt between those lines only leaves active stuck at 1. That costs speed (no more pauses), not GC correctness. Alternatively, wrap the decrement and enable in try/finally.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@peco-review-bot
peco-review-bot Bot deployed to azure-prod November 12, 2026 22:48 Active

This branch was successfully deployed

1 active deployment
azure-prod — f1d40610 Deployed Nov 12, 2026 by peco-review-bot[bot] via followup #2166
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@peco-review-bot peco-review-bot[bot] peco-review-bot[bot] approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@vuanhphung
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.