bench: model remote object storage faithfully for --simulate-latency - #24093
Draft
adriangb wants to merge 2 commits into
Draft
bench: model remote object storage faithfully for --simulate-latency#24093adriangb wants to merge 2 commits into
adriangb wants to merge 2 commits into
Conversation
…iles Adds `SimulatedObjectStore`, which presents a local filesystem the way S3 or GCS present themselves, so benchmarks can exercise realistic IO without a network. The point is fidelity of the request *pattern*, not just of the wall clock. Neither `AmazonS3` nor `GoogleCloudStorage` implements `ObjectStore::get_ranges`; they inherit the trait default, which merges ranges less than 1MiB apart and issues the merged chunks as up to 10 concurrent GETs. So this store implements only the HTTP-shaped primitives a real remote store implements and leaves `get_ranges`, `list_with_offset` and `rename_opts` to the trait defaults, letting arrow-rs's coalescing and fan-out run against it unmodified. Each simulated request pays for a connection from a bounded pool, a time to first byte drawn from an S3-shaped distribution, and transfer time at a fixed per-connection bandwidth. LIST is paginated at 1000 keys with pages charged serially, since each needs the previous continuation token. Not committed yet: the benchmark runner still uses the old store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces `LatencyObjectStore`, which overrode `get_ranges` and slept once per call. That charged one round trip where S3 pays one per coalesced chunk, and it modelled no per-byte cost at all, so a 4 byte footer read and a 100MB column chunk read cost the same. It also charged a single latency for an entire LIST stream, where the real APIs paginate at 1000 keys. Numbers from previous --simulate-latency runs are not comparable across this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24093 +/- ##
==========================================
+ Coverage 80.85% 80.90% +0.04%
==========================================
Files 1099 1103 +4
Lines 374304 376672 +2368
Branches 374304 376672 +2368
==========================================
+ Hits 302642 304739 +2097
- Misses 53607 53808 +201
- Partials 18055 18125 +70 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
|
Good idea @adriangb let me know when it's reviewable. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Which issue does this PR close?
Rationale for this change
--simulate-latencywraps the local filesystem inLatencyObjectStoreso benchmarks can be run against S3-like IO without a network. It is not a faithful model of one, in three ways that matter.It overrides
get_ranges. NeitherAmazonS3norGoogleCloudStorageimplements that method; they inherit theObjectStoredefault, which merges ranges less thanOBJECT_STORE_COALESCE_DEFAULT(1MiB) apart and issues the merged chunks as up to 10 concurrent GETs.LatencyObjectStoreoverrides it, sleeps once, and delegates toLocalFileSystem::get_ranges— so it charges one round trip where S3 pays one per coalesced chunk, and it inherits local positional reads with no coalescing at all. How many chunks a vectored read produces is data dependent (column layout, projection, gap distances), which is exactly what an IO optimisation changes, so the simulation is blind to the thing being measured.It has no per-byte cost. A 4 byte footer read and a 100MB column chunk read cost the same. Coalescing across a gap trades bytes for round trips; without a bytes term the simulation says that trade is always free.
It charges one latency for an entire LIST stream. Both S3 and GCS cap a listing response at 1000 keys, and each page needs the continuation token from the one before it, so pages are serial round trips. Listing 5000 files is five round trips, not one.
What changes are included in this PR?
Replaces
LatencyObjectStorewithSimulatedObjectStore, which presents a local filesystem the way a remote store presents itself.The organising principle is that it implements only the HTTP-shaped primitives a real remote store implements, and leaves
get_ranges,list_with_offsetandrename_optsto theObjectStoredefaults — exactly asAmazonS3andGoogleCloudStorageleave them. Everything arrow-rs does above the wire (coalescing, fan-out, pagination) then runs against the simulator unmodified.This is deliberately the opposite of the advice in
ObjectStore's "Wrappers" doc section, which tells wrappers to implement every method so they do not lose the wrapped store's overrides. That advice is for observability wrappers; here the wrapped store's overrides are the problem. The module documents this, and there is a comment against adding#[deny(clippy::missing_trait_methods)].Each simulated request pays for:
max_concurrent_requests, default 128),connection_bytes_per_second(default 100MB/s), charged as the body is consumed.A
HEADis charged (1) only — itsGetResultreports a range spanning the whole object even though no bytes move, and charging that as a transfer made every metadata probe cost as much as downloading the file.Bounded reads take a single blocking read; reads above
max_eager_read_bytesstream, so a whole-object GET of a multi-GB CSV is not buffered in memory.The latency sampler now hashes its draw counter rather than round-robining. A 20-entry table cycled against
coalesce_ranges' fixed waves of 10 meant every vectored read saw the same two fixed sets of latencies, and a read issuing exactly 10 or 20 requests always landed on the table mean.What is deliberately not modelled
arrow-rs never splits one large range into several smaller concurrent requests —
merge_rangeshas no upper bound on merged size and no store chunks a large GET — so neither does this. A 200MB coalesced range is one GET on one connection, and is bandwidth bound. Retries, multipart part uploads and per-prefix throttling are also not modelled.Are these changes tested?
Yes — 10 new tests.
The ones carrying the argument above:
distant_ranges_become_separate_requests/nearby_ranges_are_coalesced_into_one_request— ranges more than 1MiB apart reach the wire as separate requests; nearer ones are merged into one. The old store reported one in both cases.projecting_more_columns_costs_more_requests— end to end through a real DataFusion Parquet scan: two distant column chunks requested in a singleget_byte_rangescall cost more requests than one. The old store could not express this difference.head_requests_do_not_pay_for_a_body,transfer_time_scales_with_bytes,list_is_charged_per_page.cargo test -p datafusion-benchmarkspasses (133 tests).cargo clippy -p datafusion-benchmarks --all-targets --no-deps -- -D warningsis clean.Benchmark results
TPC-H SF1 parquet, 3 iterations, all 22 queries, run back to back on the same machine. "old sim" is
LatencyObjectStoreat the parent commit; "new sim" is this PR. Both with--simulate-latency.The total is not the interesting number. The spread is: per query, new/old ranges from 0.90x to 2.04x, because the new model differentiates queries that the old one charged identically.
Queries whose vectored reads fan out into many distant chunks (q22, q21) get materially more expensive; the small decreases at the bottom of the table are within run-to-run variance and I would not read anything into them. Relative query ranking changes between the two models, which is the point: an IO change evaluated under the old model was being scored against a cost function that could not see request count.
Numbers from previous
--simulate-latencyruns are not comparable across this change.Are there any user-facing changes?
No public API changes — this is benchmark-only.
--simulate-latencyandSIMULATE_LATENCYkeep their names and meaning; their help text andbench.shdocumentation are updated to describe what is now modelled.🤖 Generated with Claude Code