@rocicorp/zero 1.12.0-canary.11 → 1.12.0-canary.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/replicache/src/btree/diff.d.ts.map +1 -1
- package/out/replicache/src/btree/diff.js +1 -2
- package/out/replicache/src/btree/diff.js.map +1 -1
- package/out/replicache/src/btree/read.d.ts +31 -1
- package/out/replicache/src/btree/read.d.ts.map +1 -1
- package/out/replicache/src/btree/read.js +74 -19
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/btree/splice.d.ts.map +1 -1
- package/out/replicache/src/btree/splice.js +2 -1
- package/out/replicache/src/btree/splice.js.map +1 -1
- package/out/replicache/src/btree/write.d.ts.map +1 -1
- package/out/replicache/src/btree/write.js +2 -1
- package/out/replicache/src/btree/write.js.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.d.ts +1 -0
- package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js +13 -5
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/scan-iterator.js +1 -1
- package/out/replicache/src/sync/request-id.js +2 -2
- package/out/replicache/src/sync/request-id.js.map +1 -1
- package/out/shared/src/bigint-json.d.ts.map +1 -1
- package/out/shared/src/bigint-json.js +1 -0
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/btree-set.d.ts +21 -0
- package/out/shared/src/btree-set.d.ts.map +1 -1
- package/out/shared/src/btree-set.js +54 -0
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/headers.js +20 -0
- package/out/shared/src/headers.js.map +1 -0
- package/out/shared/src/natural-runs.d.ts +191 -0
- package/out/shared/src/natural-runs.d.ts.map +1 -0
- package/out/shared/src/natural-runs.js +337 -0
- package/out/shared/src/natural-runs.js.map +1 -0
- package/out/shared/src/random-uint64.d.ts.map +1 -1
- package/out/shared/src/random-uint64.js +2 -2
- package/out/shared/src/random-uint64.js.map +1 -1
- package/out/shared/src/random-values.d.ts +19 -1
- package/out/shared/src/random-values.d.ts.map +1 -1
- package/out/shared/src/random-values.js +38 -4
- package/out/shared/src/random-values.js.map +1 -1
- package/out/shared/src/tuple-map.d.ts +22 -0
- package/out/shared/src/tuple-map.d.ts.map +1 -0
- package/out/shared/src/tuple-map.js +48 -0
- package/out/shared/src/tuple-map.js.map +1 -0
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +5 -0
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +21 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +47 -9
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js +3 -2
- package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
- package/out/zero-cache/src/db/pg-type-parser.js +55 -2
- package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +5 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +6 -29
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/main.js +5 -0
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.js +7 -4
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/serving-checkpointer.js +41 -0
- package/out/zero-cache/src/server/serving-checkpointer.js.map +1 -0
- package/out/zero-cache/src/server/syncer.js +5 -4
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
- package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.d.ts +1 -0
- package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js +2 -1
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
- package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
- package/out/zero-cache/src/services/admin-auth.js +42 -0
- package/out/zero-cache/src/services/admin-auth.js.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +13 -7
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
- package/out/zero-cache/src/services/heapz.js +4 -5
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.d.ts +5 -0
- package/out/zero-cache/src/services/mutagen/error.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.js +7 -1
- package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
- package/out/zero-cache/src/services/plannerz.js +3 -7
- package/out/zero-cache/src/services/plannerz.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +4 -13
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +29 -2
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/serving-checkpointer.js +85 -0
- package/out/zero-cache/src/services/replicator/serving-checkpointer.js.map +1 -0
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +2 -7
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +3 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +16 -14
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
- package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +90 -52
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
- package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
- package/out/zero-cache/src/types/client-group-log-id.js +16 -0
- package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +1 -0
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts +13 -2
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +2 -2
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.d.ts +25 -4
- package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.js +46 -5
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +10 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/rehome.d.ts +10 -0
- package/out/zero-cache/src/workers/rehome.d.ts.map +1 -1
- package/out/zero-cache/src/workers/rehome.js +4 -2
- package/out/zero-cache/src/workers/rehome.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +9 -4
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +3 -2
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +15 -15
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
- package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js +4 -0
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/query.d.ts +2 -2
- package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/query.js +4 -4
- package/out/zero-client/src/client/inspector/query.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts +3 -0
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +10 -5
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/reconnect-backoff.d.ts +31 -0
- package/out/zero-client/src/client/reconnect-backoff.d.ts.map +1 -0
- package/out/zero-client/src/client/reconnect-backoff.js +44 -0
- package/out/zero-client/src/client/reconnect-backoff.js.map +1 -0
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +7 -0
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/util/nanoid.js +2 -2
- package/out/zero-client/src/util/nanoid.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +42 -42
- package/out/zero-server/src/process-mutations.d.ts +10 -0
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +39 -6
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +89 -6
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
- package/out/zql/src/planner/planner-connection.js +37 -3
- package/out/zql/src/planner/planner-connection.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable sort for input that already arrives in a handful of ordered runs,
|
|
3
|
+
* for the engines whose built-in sort cannot exploit that.
|
|
4
|
+
*
|
|
5
|
+
* `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1
|
|
6
|
+
* comparisons. **On Hermes it is not adaptive**, and the difference is not
|
|
7
|
+
* small. Sorting 100,000 objects through a two-column comparator:
|
|
8
|
+
*
|
|
9
|
+
* ```text
|
|
10
|
+
* Hermes 0.12 V8
|
|
11
|
+
* sorted 93.6ms / 1,483,630 0.8ms / 99,999
|
|
12
|
+
* reversed 168.6ms / 2,571,203 0.8ms / 99,999
|
|
13
|
+
* shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Hermes does fifteen times the necessary comparisons on sorted input, and
|
|
17
|
+
* reversed input is its *worst* case -- slower than random data, where an O(N)
|
|
18
|
+
* reverse would do. That matters because ordered input is the common case here
|
|
19
|
+
* and not a lucky one: rows come out of a source in primary-key order, and
|
|
20
|
+
* secondary sort keys are usually timestamps or sequence numbers that move with
|
|
21
|
+
* insertion order, so a secondary index is often built over rows that are
|
|
22
|
+
* already sorted, exactly reversed, or a few ordered batches concatenated.
|
|
23
|
+
*
|
|
24
|
+
* This merges the natural runs instead, which is O(N) on one run and
|
|
25
|
+
* O(N log R) on R of them. Measured against the same inputs:
|
|
26
|
+
*
|
|
27
|
+
* ```text
|
|
28
|
+
* Hermes native -> runs V8 native -> runs
|
|
29
|
+
* sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms
|
|
30
|
+
* reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms
|
|
31
|
+
* 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ## Why it gives up rather than always merging
|
|
35
|
+
*
|
|
36
|
+
* A full TimSort would handle near-sorted input too, but without galloping it
|
|
37
|
+
* loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000
|
|
38
|
+
* near-sorted rows, 1.26M comparisons against 268k), and galloping is a large
|
|
39
|
+
* amount of code to carry. So this returns `undefined` once the input has more
|
|
40
|
+
* runs than merging can pay for, and the caller falls back to whatever it would
|
|
41
|
+
* have done -- which keeps V8's galloping and costs Hermes nothing it was not
|
|
42
|
+
* already paying.
|
|
43
|
+
*
|
|
44
|
+
* ## Where the bound sits, and why it scales
|
|
45
|
+
*
|
|
46
|
+
* Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort
|
|
47
|
+
* costs about `n * log2(n)` whatever the input looks like. So merging is worth
|
|
48
|
+
* it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is
|
|
49
|
+
* the region where it is at most half -- a bound that needs no fitted constant
|
|
50
|
+
* and tightens on its own as `n` grows. That is what `maxRuns` defaults to,
|
|
51
|
+
* floored at 32 so nothing below 1,024 elements changes.
|
|
52
|
+
*
|
|
53
|
+
* Read the other way round, that bound is a statement about the *input* rather
|
|
54
|
+
* than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying
|
|
55
|
+
* the average run is at least `sqrt(n)` long. Which is the useful form, because
|
|
56
|
+
* what makes merging pay is runs long enough to amortise a merge pass over, and
|
|
57
|
+
* that does not change when a data set grows. A fixed run count does: the
|
|
58
|
+
* bound was 32 until a table with 50 of something turned up, which is the
|
|
59
|
+
* failure mode this shape avoids.
|
|
60
|
+
*
|
|
61
|
+
* The floor used to be the whole bound, and a fixed 32 was too low to catch the
|
|
62
|
+
* shape this was written for. A secondary index built over a junction table
|
|
63
|
+
* whose primary key leads with the *other* column arrives as one ordered run
|
|
64
|
+
* per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a
|
|
65
|
+
* table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it
|
|
66
|
+
* declined and Hermes fell through to the full non-adaptive sort this function
|
|
67
|
+
* exists to avoid.
|
|
68
|
+
*
|
|
69
|
+
* The bound is deliberately well inside the crossover, and comparisons are not
|
|
70
|
+
* the only cost: merging R runs is `ceil(log2(R))` passes, each writing every
|
|
71
|
+
* element, which an in-place sort does not do. So it was measured past the
|
|
72
|
+
* bound rather than up to it. Sorting 60,000 rows through a three-column
|
|
73
|
+
* comparator on an Android emulator, in a release build, medians of 3 repeats.
|
|
74
|
+
* The engine is Static Hermes 250829098.0.14 -- read out of the APK's
|
|
75
|
+
* `libhermesvm.so` rather than taken from the build config, because what React
|
|
76
|
+
* Native ships is not the engine `sdks/.hermesversion` pins. Hence
|
|
77
|
+
* `libhermesvm.so` and not `libhermes.so`: a different engine, not a later
|
|
78
|
+
* build of the one in the 0.12 table above.
|
|
79
|
+
*
|
|
80
|
+
* Which does not make that table stale, and it is worth being exact about what
|
|
81
|
+
* carries over. Its *comparison counts* reproduce on the shipped engine to the
|
|
82
|
+
* digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at
|
|
83
|
+
* n = 128 -- so the two engines sort by the same algorithm, and the premise all
|
|
84
|
+
* of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on
|
|
85
|
+
* the VM that actually runs, not merely on an old CLI. Its *times* do not carry
|
|
86
|
+
* over, in either direction: those were a standalone CLI on a development host,
|
|
87
|
+
* these are an emulator, and the shipped VM is about 2.3x faster than that CLI
|
|
88
|
+
* on the same host. Compare the counts, never the milliseconds.
|
|
89
|
+
*
|
|
90
|
+
* ```text
|
|
91
|
+
* runs native merge
|
|
92
|
+
* 2 517.9ms 23.0ms
|
|
93
|
+
* 50 218.3ms 82.0ms
|
|
94
|
+
* 100 221.6ms 90.3ms
|
|
95
|
+
* 244 202.8ms 101.4ms <- the bound at this size
|
|
96
|
+
* 400 210.7ms 110.9ms
|
|
97
|
+
* 1,000 199.4ms 121.5ms
|
|
98
|
+
* shuffled 221.0ms 207.3ms
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* The extra passes do show up -- merging costs five times more at 1,000 runs
|
|
102
|
+
* than at 2 -- but on this engine it still wins everywhere measured, and only
|
|
103
|
+
* on shuffled input does the margin close to nothing. At the bound it is 2x,
|
|
104
|
+
* which is the margin the bound is chosen to keep rather than the point where
|
|
105
|
+
* merging stops paying; a higher one would capture more, and no workload here
|
|
106
|
+
* calls for it. Note also that two ordered runs are the native sort's *worst*
|
|
107
|
+
* case (517.9ms, worse than shuffled), which is the asymmetry this whole
|
|
108
|
+
* function exists to exploit.
|
|
109
|
+
*
|
|
110
|
+
* On V8, which never reaches this path, merging holds level to about 1,000 runs
|
|
111
|
+
* (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms
|
|
112
|
+
* against 43.0ms). That bounds how far merging scales on an adaptive engine; it
|
|
113
|
+
* does not justify the bound, since no caller gets here on one.
|
|
114
|
+
*
|
|
115
|
+
* Declining is not free, and what bounds it is runs, not comparisons: counting
|
|
116
|
+
* stops once a `maxRuns + 1`th run is known to exist, before walking that one,
|
|
117
|
+
* but every run before it is walked to its end. Measured over 100,000 elements
|
|
118
|
+
* with `maxRuns` at 32, against the comparisons the native sort then does:
|
|
119
|
+
*
|
|
120
|
+
* ```text
|
|
121
|
+
* shuffled 76 comparisons 0.005%
|
|
122
|
+
* 32 long runs, then more 80,000 comparisons 15%
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* Raising the bound raises what a decline can cost, and the shape that pays is
|
|
126
|
+
* an input whose run count lands just past it. Over 60,000 elements in equal
|
|
127
|
+
* ordered runs, comparisons spent before declining:
|
|
128
|
+
*
|
|
129
|
+
* ```text
|
|
130
|
+
* runs at bound 32 at bound 244
|
|
131
|
+
* 245 7,840 59,780
|
|
132
|
+
* 400 4,800 36,600
|
|
133
|
+
* 1,000 1,920 14,640
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* So a 245-run input now scans essentially the whole array where it used to
|
|
137
|
+
* stop after an eighth of it. What does *not* change is the ceiling: the worst
|
|
138
|
+
* case is `maxRuns` long runs followed by anything at all, and that walks
|
|
139
|
+
* almost every element at either bound -- 60,000 comparisons at 32 against
|
|
140
|
+
* 59,780 at 244. The old bound never bounded the scan below `n`; it only made
|
|
141
|
+
* fewer shapes reach it. Against the non-adaptive sort a decline falls into,
|
|
142
|
+
* which costs about 14.8 comparisons per element on the engine that gets here,
|
|
143
|
+
* a full scan is around 7% on top. (Measured as a fraction of V8's adaptive
|
|
144
|
+
* sort it looks like 17-19%, but nothing reaches this path on V8.)
|
|
145
|
+
*
|
|
146
|
+
* A scan budget cannot be separated from the merge budget without giving up
|
|
147
|
+
* merges that would have won: `R` is only known to exceed the budget once
|
|
148
|
+
* budget-many runs have been seen, and merging `R` runs has to scan all `n`
|
|
149
|
+
* elements regardless. Any limit tight enough to cut a decline short of `n`
|
|
150
|
+
* also rejects inputs just inside the bound, which is the case this change
|
|
151
|
+
* exists to serve.
|
|
152
|
+
*
|
|
153
|
+
* One interaction is worth naming because this bound makes it bigger. The
|
|
154
|
+
* caller in `MemorySource` tries this before its specialised numeric-column
|
|
155
|
+
* pass, so an ordering that the numeric pass could have handled may now pay a
|
|
156
|
+
* scan of up to `n` before taking it anyway. Bounded, and not a shape seen in
|
|
157
|
+
* the ten index builds of a cold pass -- the nine that merge are under 32 runs
|
|
158
|
+
* and the one that declines is string-keyed -- but the chain order is worth
|
|
159
|
+
* revisiting on its own measurement rather than inside a bound change.
|
|
160
|
+
*
|
|
161
|
+
* Of the two rows in the first table, shuffled is the shape that declines in
|
|
162
|
+
* practice, and it gives up almost at once because its runs are two or three
|
|
163
|
+
* elements long. The other is the honest worst case, where the scan walks most
|
|
164
|
+
* of the array before handing it back. Nothing in the
|
|
165
|
+
* workloads this was built for looks like that, but it is what the bound
|
|
166
|
+
* permits, and a caller sorting adversarial input should know it.
|
|
167
|
+
*
|
|
168
|
+
* ## Stability
|
|
169
|
+
*
|
|
170
|
+
* Stable, and that is load-bearing rather than incidental: callers rely on
|
|
171
|
+
* equal elements keeping their input order. Two rules preserve it. Descending
|
|
172
|
+
* runs are detected **strictly**, so equal neighbours are never part of a run
|
|
173
|
+
* that gets reversed; and the merge takes from the earlier run when its head
|
|
174
|
+
* compares equal. The counting pass also does not mutate, so an array handed
|
|
175
|
+
* back on the `undefined` path is untouched -- reversing runs first and then
|
|
176
|
+
* falling back would hand the caller's sort an array whose equal elements had
|
|
177
|
+
* already been reordered, which is how a stable sort quietly stops being one.
|
|
178
|
+
*/
|
|
179
|
+
export declare function nativeSortIsAdaptive(): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Sorts `array` in place by merging its natural runs, and returns it.
|
|
182
|
+
*
|
|
183
|
+
* Returns `undefined`, leaving `array` **unmodified**, when it holds more than
|
|
184
|
+
* `maxRuns` runs -- the caller should then sort it however it otherwise would.
|
|
185
|
+
* `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see "Where the
|
|
186
|
+
* bound sits" above.
|
|
187
|
+
*
|
|
188
|
+
* `compare` must be a consistent comparator; results are stable.
|
|
189
|
+
*/
|
|
190
|
+
export declare function sortByNaturalRuns<T>(array: T[], compare: (a: T, b: T) => number, maxRuns?: number): T[] | undefined;
|
|
191
|
+
//# sourceMappingURL=natural-runs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-runs.d.ts","sourceRoot":"","sources":["../../../../shared/src/natural-runs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiLG;AAgDH,wBAAgB,oBAAoB,IAAI,OAAO,CAc9C;AAWD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,EAC/B,OAAO,SAA2B,GACjC,CAAC,EAAE,GAAG,SAAS,CAoJjB"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
//#region ../shared/src/natural-runs.ts
|
|
2
|
+
/**
|
|
3
|
+
* A stable sort for input that already arrives in a handful of ordered runs,
|
|
4
|
+
* for the engines whose built-in sort cannot exploit that.
|
|
5
|
+
*
|
|
6
|
+
* `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1
|
|
7
|
+
* comparisons. **On Hermes it is not adaptive**, and the difference is not
|
|
8
|
+
* small. Sorting 100,000 objects through a two-column comparator:
|
|
9
|
+
*
|
|
10
|
+
* ```text
|
|
11
|
+
* Hermes 0.12 V8
|
|
12
|
+
* sorted 93.6ms / 1,483,630 0.8ms / 99,999
|
|
13
|
+
* reversed 168.6ms / 2,571,203 0.8ms / 99,999
|
|
14
|
+
* shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Hermes does fifteen times the necessary comparisons on sorted input, and
|
|
18
|
+
* reversed input is its *worst* case -- slower than random data, where an O(N)
|
|
19
|
+
* reverse would do. That matters because ordered input is the common case here
|
|
20
|
+
* and not a lucky one: rows come out of a source in primary-key order, and
|
|
21
|
+
* secondary sort keys are usually timestamps or sequence numbers that move with
|
|
22
|
+
* insertion order, so a secondary index is often built over rows that are
|
|
23
|
+
* already sorted, exactly reversed, or a few ordered batches concatenated.
|
|
24
|
+
*
|
|
25
|
+
* This merges the natural runs instead, which is O(N) on one run and
|
|
26
|
+
* O(N log R) on R of them. Measured against the same inputs:
|
|
27
|
+
*
|
|
28
|
+
* ```text
|
|
29
|
+
* Hermes native -> runs V8 native -> runs
|
|
30
|
+
* sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms
|
|
31
|
+
* reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms
|
|
32
|
+
* 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* ## Why it gives up rather than always merging
|
|
36
|
+
*
|
|
37
|
+
* A full TimSort would handle near-sorted input too, but without galloping it
|
|
38
|
+
* loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000
|
|
39
|
+
* near-sorted rows, 1.26M comparisons against 268k), and galloping is a large
|
|
40
|
+
* amount of code to carry. So this returns `undefined` once the input has more
|
|
41
|
+
* runs than merging can pay for, and the caller falls back to whatever it would
|
|
42
|
+
* have done -- which keeps V8's galloping and costs Hermes nothing it was not
|
|
43
|
+
* already paying.
|
|
44
|
+
*
|
|
45
|
+
* ## Where the bound sits, and why it scales
|
|
46
|
+
*
|
|
47
|
+
* Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort
|
|
48
|
+
* costs about `n * log2(n)` whatever the input looks like. So merging is worth
|
|
49
|
+
* it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is
|
|
50
|
+
* the region where it is at most half -- a bound that needs no fitted constant
|
|
51
|
+
* and tightens on its own as `n` grows. That is what `maxRuns` defaults to,
|
|
52
|
+
* floored at 32 so nothing below 1,024 elements changes.
|
|
53
|
+
*
|
|
54
|
+
* Read the other way round, that bound is a statement about the *input* rather
|
|
55
|
+
* than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying
|
|
56
|
+
* the average run is at least `sqrt(n)` long. Which is the useful form, because
|
|
57
|
+
* what makes merging pay is runs long enough to amortise a merge pass over, and
|
|
58
|
+
* that does not change when a data set grows. A fixed run count does: the
|
|
59
|
+
* bound was 32 until a table with 50 of something turned up, which is the
|
|
60
|
+
* failure mode this shape avoids.
|
|
61
|
+
*
|
|
62
|
+
* The floor used to be the whole bound, and a fixed 32 was too low to catch the
|
|
63
|
+
* shape this was written for. A secondary index built over a junction table
|
|
64
|
+
* whose primary key leads with the *other* column arrives as one ordered run
|
|
65
|
+
* per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a
|
|
66
|
+
* table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it
|
|
67
|
+
* declined and Hermes fell through to the full non-adaptive sort this function
|
|
68
|
+
* exists to avoid.
|
|
69
|
+
*
|
|
70
|
+
* The bound is deliberately well inside the crossover, and comparisons are not
|
|
71
|
+
* the only cost: merging R runs is `ceil(log2(R))` passes, each writing every
|
|
72
|
+
* element, which an in-place sort does not do. So it was measured past the
|
|
73
|
+
* bound rather than up to it. Sorting 60,000 rows through a three-column
|
|
74
|
+
* comparator on an Android emulator, in a release build, medians of 3 repeats.
|
|
75
|
+
* The engine is Static Hermes 250829098.0.14 -- read out of the APK's
|
|
76
|
+
* `libhermesvm.so` rather than taken from the build config, because what React
|
|
77
|
+
* Native ships is not the engine `sdks/.hermesversion` pins. Hence
|
|
78
|
+
* `libhermesvm.so` and not `libhermes.so`: a different engine, not a later
|
|
79
|
+
* build of the one in the 0.12 table above.
|
|
80
|
+
*
|
|
81
|
+
* Which does not make that table stale, and it is worth being exact about what
|
|
82
|
+
* carries over. Its *comparison counts* reproduce on the shipped engine to the
|
|
83
|
+
* digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at
|
|
84
|
+
* n = 128 -- so the two engines sort by the same algorithm, and the premise all
|
|
85
|
+
* of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on
|
|
86
|
+
* the VM that actually runs, not merely on an old CLI. Its *times* do not carry
|
|
87
|
+
* over, in either direction: those were a standalone CLI on a development host,
|
|
88
|
+
* these are an emulator, and the shipped VM is about 2.3x faster than that CLI
|
|
89
|
+
* on the same host. Compare the counts, never the milliseconds.
|
|
90
|
+
*
|
|
91
|
+
* ```text
|
|
92
|
+
* runs native merge
|
|
93
|
+
* 2 517.9ms 23.0ms
|
|
94
|
+
* 50 218.3ms 82.0ms
|
|
95
|
+
* 100 221.6ms 90.3ms
|
|
96
|
+
* 244 202.8ms 101.4ms <- the bound at this size
|
|
97
|
+
* 400 210.7ms 110.9ms
|
|
98
|
+
* 1,000 199.4ms 121.5ms
|
|
99
|
+
* shuffled 221.0ms 207.3ms
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* The extra passes do show up -- merging costs five times more at 1,000 runs
|
|
103
|
+
* than at 2 -- but on this engine it still wins everywhere measured, and only
|
|
104
|
+
* on shuffled input does the margin close to nothing. At the bound it is 2x,
|
|
105
|
+
* which is the margin the bound is chosen to keep rather than the point where
|
|
106
|
+
* merging stops paying; a higher one would capture more, and no workload here
|
|
107
|
+
* calls for it. Note also that two ordered runs are the native sort's *worst*
|
|
108
|
+
* case (517.9ms, worse than shuffled), which is the asymmetry this whole
|
|
109
|
+
* function exists to exploit.
|
|
110
|
+
*
|
|
111
|
+
* On V8, which never reaches this path, merging holds level to about 1,000 runs
|
|
112
|
+
* (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms
|
|
113
|
+
* against 43.0ms). That bounds how far merging scales on an adaptive engine; it
|
|
114
|
+
* does not justify the bound, since no caller gets here on one.
|
|
115
|
+
*
|
|
116
|
+
* Declining is not free, and what bounds it is runs, not comparisons: counting
|
|
117
|
+
* stops once a `maxRuns + 1`th run is known to exist, before walking that one,
|
|
118
|
+
* but every run before it is walked to its end. Measured over 100,000 elements
|
|
119
|
+
* with `maxRuns` at 32, against the comparisons the native sort then does:
|
|
120
|
+
*
|
|
121
|
+
* ```text
|
|
122
|
+
* shuffled 76 comparisons 0.005%
|
|
123
|
+
* 32 long runs, then more 80,000 comparisons 15%
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* Raising the bound raises what a decline can cost, and the shape that pays is
|
|
127
|
+
* an input whose run count lands just past it. Over 60,000 elements in equal
|
|
128
|
+
* ordered runs, comparisons spent before declining:
|
|
129
|
+
*
|
|
130
|
+
* ```text
|
|
131
|
+
* runs at bound 32 at bound 244
|
|
132
|
+
* 245 7,840 59,780
|
|
133
|
+
* 400 4,800 36,600
|
|
134
|
+
* 1,000 1,920 14,640
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* So a 245-run input now scans essentially the whole array where it used to
|
|
138
|
+
* stop after an eighth of it. What does *not* change is the ceiling: the worst
|
|
139
|
+
* case is `maxRuns` long runs followed by anything at all, and that walks
|
|
140
|
+
* almost every element at either bound -- 60,000 comparisons at 32 against
|
|
141
|
+
* 59,780 at 244. The old bound never bounded the scan below `n`; it only made
|
|
142
|
+
* fewer shapes reach it. Against the non-adaptive sort a decline falls into,
|
|
143
|
+
* which costs about 14.8 comparisons per element on the engine that gets here,
|
|
144
|
+
* a full scan is around 7% on top. (Measured as a fraction of V8's adaptive
|
|
145
|
+
* sort it looks like 17-19%, but nothing reaches this path on V8.)
|
|
146
|
+
*
|
|
147
|
+
* A scan budget cannot be separated from the merge budget without giving up
|
|
148
|
+
* merges that would have won: `R` is only known to exceed the budget once
|
|
149
|
+
* budget-many runs have been seen, and merging `R` runs has to scan all `n`
|
|
150
|
+
* elements regardless. Any limit tight enough to cut a decline short of `n`
|
|
151
|
+
* also rejects inputs just inside the bound, which is the case this change
|
|
152
|
+
* exists to serve.
|
|
153
|
+
*
|
|
154
|
+
* One interaction is worth naming because this bound makes it bigger. The
|
|
155
|
+
* caller in `MemorySource` tries this before its specialised numeric-column
|
|
156
|
+
* pass, so an ordering that the numeric pass could have handled may now pay a
|
|
157
|
+
* scan of up to `n` before taking it anyway. Bounded, and not a shape seen in
|
|
158
|
+
* the ten index builds of a cold pass -- the nine that merge are under 32 runs
|
|
159
|
+
* and the one that declines is string-keyed -- but the chain order is worth
|
|
160
|
+
* revisiting on its own measurement rather than inside a bound change.
|
|
161
|
+
*
|
|
162
|
+
* Of the two rows in the first table, shuffled is the shape that declines in
|
|
163
|
+
* practice, and it gives up almost at once because its runs are two or three
|
|
164
|
+
* elements long. The other is the honest worst case, where the scan walks most
|
|
165
|
+
* of the array before handing it back. Nothing in the
|
|
166
|
+
* workloads this was built for looks like that, but it is what the bound
|
|
167
|
+
* permits, and a caller sorting adversarial input should know it.
|
|
168
|
+
*
|
|
169
|
+
* ## Stability
|
|
170
|
+
*
|
|
171
|
+
* Stable, and that is load-bearing rather than incidental: callers rely on
|
|
172
|
+
* equal elements keeping their input order. Two rules preserve it. Descending
|
|
173
|
+
* runs are detected **strictly**, so equal neighbours are never part of a run
|
|
174
|
+
* that gets reversed; and the merge takes from the earlier run when its head
|
|
175
|
+
* compares equal. The counting pass also does not mutate, so an array handed
|
|
176
|
+
* back on the `undefined` path is untouched -- reversing runs first and then
|
|
177
|
+
* falling back would hand the caller's sort an array whose equal elements had
|
|
178
|
+
* already been reordered, which is how a stable sort quietly stops being one.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* Whether this engine's `Array.prototype.sort` already exploits ordered input,
|
|
182
|
+
* measured rather than guessed from the user agent.
|
|
183
|
+
*
|
|
184
|
+
* Sorting an already-sorted array of `PROBE_SIZE` elements takes exactly
|
|
185
|
+
* `n - 1` comparisons on an adaptive sort and several times that otherwise.
|
|
186
|
+
* Measured at `n = 128`: V8 does 127, Hermes does 741. The threshold below sits
|
|
187
|
+
* between them with a wide margin at every size either engine was tried at.
|
|
188
|
+
*
|
|
189
|
+
* This exists so `sortByNaturalRuns` can be skipped where it cannot win. On V8
|
|
190
|
+
* the native sort is already optimal on ordered input, so run detection only
|
|
191
|
+
* adds a JS pass over data a faster native pass would have walked anyway --
|
|
192
|
+
* measured at a 6% regression on cold-start hydration when applied there
|
|
193
|
+
* unconditionally.
|
|
194
|
+
*
|
|
195
|
+
* The probe runs once, costs a few hundred comparisons, and is deliberately not
|
|
196
|
+
* consulted by `sortByNaturalRuns` itself: callers decide, so the sort stays a
|
|
197
|
+
* pure function that can be tested on whichever engine the tests run on.
|
|
198
|
+
*
|
|
199
|
+
* ## Why this is measured at runtime rather than compiled away
|
|
200
|
+
*
|
|
201
|
+
* The obvious alternative is a build-time flag, so that only the React Native
|
|
202
|
+
* build carries this file at all -- 1,064 bytes minified, 590 gzipped. It was
|
|
203
|
+
* considered and rejected, because "React Native" is a proxy for "Hermes" and
|
|
204
|
+
* is wrong in both directions: React Native can run on JavaScriptCore, as it
|
|
205
|
+
* did by default for years, and Hermes is embedded outside React Native. A
|
|
206
|
+
* build keyed on the former would run the natural-run sort on an engine whose
|
|
207
|
+
* own sort is already adaptive, and doing that is not merely wasted work: it
|
|
208
|
+
* measured a 6.4% regression on cold-start hydration. The probe itself is not
|
|
209
|
+
* what costs anything -- a few hundred comparisons, once: 127 where the native
|
|
210
|
+
* sort is adaptive, 741 on Hermes, which is the one that pays most and the one
|
|
211
|
+
* that gets the 17x back.
|
|
212
|
+
*
|
|
213
|
+
* Measured, sorting 128 already-sorted elements: V8 and JavaScriptCore each do
|
|
214
|
+
* exactly `n - 1` comparisons, Hermes 741. SpiderMonkey was not measured, and
|
|
215
|
+
* that is the argument rather than a hole in it. A hand-maintained list of
|
|
216
|
+
* engines would have to be right about SpiderMonkey, about whatever ships next,
|
|
217
|
+
* and about every embedding of each -- and would be wrong silently. The probe
|
|
218
|
+
* is right about all of them without being told: if SpiderMonkey turns out not
|
|
219
|
+
* to be adaptive, Firefox gets the faster path automatically, and if it is, it
|
|
220
|
+
* never pays for one it does not need.
|
|
221
|
+
*/
|
|
222
|
+
var PROBE_SIZE = 128;
|
|
223
|
+
var adaptive;
|
|
224
|
+
function nativeSortIsAdaptive() {
|
|
225
|
+
if (adaptive === void 0) {
|
|
226
|
+
const probe = [];
|
|
227
|
+
for (let i = 0; i < PROBE_SIZE; i++) probe.push(i);
|
|
228
|
+
let comparisons = 0;
|
|
229
|
+
probe.sort((a, b) => {
|
|
230
|
+
comparisons++;
|
|
231
|
+
return a - b;
|
|
232
|
+
});
|
|
233
|
+
adaptive = comparisons < 256;
|
|
234
|
+
}
|
|
235
|
+
return adaptive;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The default run budget for an array of `n` elements: `sqrt(n)`, floored at
|
|
239
|
+
* 32. Integral, because the scan compares its run count against it for
|
|
240
|
+
* equality.
|
|
241
|
+
*/
|
|
242
|
+
function maxRunsFor(n) {
|
|
243
|
+
return Math.max(32, Math.floor(Math.sqrt(n)));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Sorts `array` in place by merging its natural runs, and returns it.
|
|
247
|
+
*
|
|
248
|
+
* Returns `undefined`, leaving `array` **unmodified**, when it holds more than
|
|
249
|
+
* `maxRuns` runs -- the caller should then sort it however it otherwise would.
|
|
250
|
+
* `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see "Where the
|
|
251
|
+
* bound sits" above.
|
|
252
|
+
*
|
|
253
|
+
* `compare` must be a consistent comparator; results are stable.
|
|
254
|
+
*/
|
|
255
|
+
function sortByNaturalRuns(array, compare, maxRuns = maxRunsFor(array.length)) {
|
|
256
|
+
const n = array.length;
|
|
257
|
+
if (n < 2) return array;
|
|
258
|
+
const starts = [];
|
|
259
|
+
const descending = [];
|
|
260
|
+
let i = 0;
|
|
261
|
+
while (i < n) {
|
|
262
|
+
if (starts.length >= maxRuns) return;
|
|
263
|
+
const start = i;
|
|
264
|
+
let isDescending = false;
|
|
265
|
+
if (i + 1 < n) {
|
|
266
|
+
const first = compare(array[i + 1], array[i]);
|
|
267
|
+
if (first !== first) return;
|
|
268
|
+
isDescending = first < 0;
|
|
269
|
+
i += 2;
|
|
270
|
+
if (isDescending) while (i < n) {
|
|
271
|
+
const c = compare(array[i], array[i - 1]);
|
|
272
|
+
if (c !== c) return;
|
|
273
|
+
if (c >= 0) break;
|
|
274
|
+
i++;
|
|
275
|
+
}
|
|
276
|
+
else while (i < n) {
|
|
277
|
+
const c = compare(array[i], array[i - 1]);
|
|
278
|
+
if (c !== c) return;
|
|
279
|
+
if (c < 0) break;
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
} else i++;
|
|
283
|
+
starts.push(start);
|
|
284
|
+
descending.push(isDescending);
|
|
285
|
+
}
|
|
286
|
+
starts.push(n);
|
|
287
|
+
let runCount = descending.length;
|
|
288
|
+
if (runCount === 1) {
|
|
289
|
+
if (descending[0]) reverse(array, 0, n - 1);
|
|
290
|
+
return array;
|
|
291
|
+
}
|
|
292
|
+
let src = array.slice();
|
|
293
|
+
let dst = new Array(n);
|
|
294
|
+
let bounds = starts;
|
|
295
|
+
for (let r = 0; r < runCount; r++) if (descending[r]) reverse(src, starts[r], starts[r + 1] - 1);
|
|
296
|
+
while (runCount > 1) {
|
|
297
|
+
const nextBounds = [0];
|
|
298
|
+
let write = 0;
|
|
299
|
+
let merged = 0;
|
|
300
|
+
for (let p = 0; p < runCount; p += 2) {
|
|
301
|
+
let a = bounds[p];
|
|
302
|
+
const endA = bounds[p + 1];
|
|
303
|
+
if (p + 1 >= runCount) while (a < endA) dst[write++] = src[a++];
|
|
304
|
+
else {
|
|
305
|
+
let b = bounds[p + 1];
|
|
306
|
+
const endB = bounds[p + 2];
|
|
307
|
+
while (a < endA && b < endB) {
|
|
308
|
+
const c = compare(src[b], src[a]);
|
|
309
|
+
if (c !== c) return;
|
|
310
|
+
dst[write++] = c < 0 ? src[b++] : src[a++];
|
|
311
|
+
}
|
|
312
|
+
while (a < endA) dst[write++] = src[a++];
|
|
313
|
+
while (b < endB) dst[write++] = src[b++];
|
|
314
|
+
}
|
|
315
|
+
merged++;
|
|
316
|
+
nextBounds.push(write);
|
|
317
|
+
}
|
|
318
|
+
const swap = src;
|
|
319
|
+
src = dst;
|
|
320
|
+
dst = swap;
|
|
321
|
+
bounds = nextBounds;
|
|
322
|
+
runCount = merged;
|
|
323
|
+
}
|
|
324
|
+
for (let c = 0; c < n; c++) array[c] = src[c];
|
|
325
|
+
return array;
|
|
326
|
+
}
|
|
327
|
+
function reverse(array, lo, hi) {
|
|
328
|
+
while (lo < hi) {
|
|
329
|
+
const t = array[lo];
|
|
330
|
+
array[lo++] = array[hi];
|
|
331
|
+
array[hi--] = t;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
export { nativeSortIsAdaptive, sortByNaturalRuns };
|
|
336
|
+
|
|
337
|
+
//# sourceMappingURL=natural-runs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-runs.js","names":[],"sources":["../../../../shared/src/natural-runs.ts"],"sourcesContent":["/**\n * A stable sort for input that already arrives in a handful of ordered runs,\n * for the engines whose built-in sort cannot exploit that.\n *\n * `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1\n * comparisons. **On Hermes it is not adaptive**, and the difference is not\n * small. Sorting 100,000 objects through a two-column comparator:\n *\n * ```text\n * Hermes 0.12 V8\n * sorted 93.6ms / 1,483,630 0.8ms / 99,999\n * reversed 168.6ms / 2,571,203 0.8ms / 99,999\n * shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373\n * ```\n *\n * Hermes does fifteen times the necessary comparisons on sorted input, and\n * reversed input is its *worst* case -- slower than random data, where an O(N)\n * reverse would do. That matters because ordered input is the common case here\n * and not a lucky one: rows come out of a source in primary-key order, and\n * secondary sort keys are usually timestamps or sequence numbers that move with\n * insertion order, so a secondary index is often built over rows that are\n * already sorted, exactly reversed, or a few ordered batches concatenated.\n *\n * This merges the natural runs instead, which is O(N) on one run and\n * O(N log R) on R of them. Measured against the same inputs:\n *\n * ```text\n * Hermes native -> runs V8 native -> runs\n * sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms\n * reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms\n * 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms\n * ```\n *\n * ## Why it gives up rather than always merging\n *\n * A full TimSort would handle near-sorted input too, but without galloping it\n * loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000\n * near-sorted rows, 1.26M comparisons against 268k), and galloping is a large\n * amount of code to carry. So this returns `undefined` once the input has more\n * runs than merging can pay for, and the caller falls back to whatever it would\n * have done -- which keeps V8's galloping and costs Hermes nothing it was not\n * already paying.\n *\n * ## Where the bound sits, and why it scales\n *\n * Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort\n * costs about `n * log2(n)` whatever the input looks like. So merging is worth\n * it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is\n * the region where it is at most half -- a bound that needs no fitted constant\n * and tightens on its own as `n` grows. That is what `maxRuns` defaults to,\n * floored at 32 so nothing below 1,024 elements changes.\n *\n * Read the other way round, that bound is a statement about the *input* rather\n * than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying\n * the average run is at least `sqrt(n)` long. Which is the useful form, because\n * what makes merging pay is runs long enough to amortise a merge pass over, and\n * that does not change when a data set grows. A fixed run count does: the\n * bound was 32 until a table with 50 of something turned up, which is the\n * failure mode this shape avoids.\n *\n * The floor used to be the whole bound, and a fixed 32 was too low to catch the\n * shape this was written for. A secondary index built over a junction table\n * whose primary key leads with the *other* column arrives as one ordered run\n * per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a\n * table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it\n * declined and Hermes fell through to the full non-adaptive sort this function\n * exists to avoid.\n *\n * The bound is deliberately well inside the crossover, and comparisons are not\n * the only cost: merging R runs is `ceil(log2(R))` passes, each writing every\n * element, which an in-place sort does not do. So it was measured past the\n * bound rather than up to it. Sorting 60,000 rows through a three-column\n * comparator on an Android emulator, in a release build, medians of 3 repeats.\n * The engine is Static Hermes 250829098.0.14 -- read out of the APK's\n * `libhermesvm.so` rather than taken from the build config, because what React\n * Native ships is not the engine `sdks/.hermesversion` pins. Hence\n * `libhermesvm.so` and not `libhermes.so`: a different engine, not a later\n * build of the one in the 0.12 table above.\n *\n * Which does not make that table stale, and it is worth being exact about what\n * carries over. Its *comparison counts* reproduce on the shipped engine to the\n * digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at\n * n = 128 -- so the two engines sort by the same algorithm, and the premise all\n * of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on\n * the VM that actually runs, not merely on an old CLI. Its *times* do not carry\n * over, in either direction: those were a standalone CLI on a development host,\n * these are an emulator, and the shipped VM is about 2.3x faster than that CLI\n * on the same host. Compare the counts, never the milliseconds.\n *\n * ```text\n * runs native merge\n * 2 517.9ms 23.0ms\n * 50 218.3ms 82.0ms\n * 100 221.6ms 90.3ms\n * 244 202.8ms 101.4ms <- the bound at this size\n * 400 210.7ms 110.9ms\n * 1,000 199.4ms 121.5ms\n * shuffled 221.0ms 207.3ms\n * ```\n *\n * The extra passes do show up -- merging costs five times more at 1,000 runs\n * than at 2 -- but on this engine it still wins everywhere measured, and only\n * on shuffled input does the margin close to nothing. At the bound it is 2x,\n * which is the margin the bound is chosen to keep rather than the point where\n * merging stops paying; a higher one would capture more, and no workload here\n * calls for it. Note also that two ordered runs are the native sort's *worst*\n * case (517.9ms, worse than shuffled), which is the asymmetry this whole\n * function exists to exploit.\n *\n * On V8, which never reaches this path, merging holds level to about 1,000 runs\n * (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms\n * against 43.0ms). That bounds how far merging scales on an adaptive engine; it\n * does not justify the bound, since no caller gets here on one.\n *\n * Declining is not free, and what bounds it is runs, not comparisons: counting\n * stops once a `maxRuns + 1`th run is known to exist, before walking that one,\n * but every run before it is walked to its end. Measured over 100,000 elements\n * with `maxRuns` at 32, against the comparisons the native sort then does:\n *\n * ```text\n * shuffled 76 comparisons 0.005%\n * 32 long runs, then more 80,000 comparisons 15%\n * ```\n *\n * Raising the bound raises what a decline can cost, and the shape that pays is\n * an input whose run count lands just past it. Over 60,000 elements in equal\n * ordered runs, comparisons spent before declining:\n *\n * ```text\n * runs at bound 32 at bound 244\n * 245 7,840 59,780\n * 400 4,800 36,600\n * 1,000 1,920 14,640\n * ```\n *\n * So a 245-run input now scans essentially the whole array where it used to\n * stop after an eighth of it. What does *not* change is the ceiling: the worst\n * case is `maxRuns` long runs followed by anything at all, and that walks\n * almost every element at either bound -- 60,000 comparisons at 32 against\n * 59,780 at 244. The old bound never bounded the scan below `n`; it only made\n * fewer shapes reach it. Against the non-adaptive sort a decline falls into,\n * which costs about 14.8 comparisons per element on the engine that gets here,\n * a full scan is around 7% on top. (Measured as a fraction of V8's adaptive\n * sort it looks like 17-19%, but nothing reaches this path on V8.)\n *\n * A scan budget cannot be separated from the merge budget without giving up\n * merges that would have won: `R` is only known to exceed the budget once\n * budget-many runs have been seen, and merging `R` runs has to scan all `n`\n * elements regardless. Any limit tight enough to cut a decline short of `n`\n * also rejects inputs just inside the bound, which is the case this change\n * exists to serve.\n *\n * One interaction is worth naming because this bound makes it bigger. The\n * caller in `MemorySource` tries this before its specialised numeric-column\n * pass, so an ordering that the numeric pass could have handled may now pay a\n * scan of up to `n` before taking it anyway. Bounded, and not a shape seen in\n * the ten index builds of a cold pass -- the nine that merge are under 32 runs\n * and the one that declines is string-keyed -- but the chain order is worth\n * revisiting on its own measurement rather than inside a bound change.\n *\n * Of the two rows in the first table, shuffled is the shape that declines in\n * practice, and it gives up almost at once because its runs are two or three\n * elements long. The other is the honest worst case, where the scan walks most\n * of the array before handing it back. Nothing in the\n * workloads this was built for looks like that, but it is what the bound\n * permits, and a caller sorting adversarial input should know it.\n *\n * ## Stability\n *\n * Stable, and that is load-bearing rather than incidental: callers rely on\n * equal elements keeping their input order. Two rules preserve it. Descending\n * runs are detected **strictly**, so equal neighbours are never part of a run\n * that gets reversed; and the merge takes from the earlier run when its head\n * compares equal. The counting pass also does not mutate, so an array handed\n * back on the `undefined` path is untouched -- reversing runs first and then\n * falling back would hand the caller's sort an array whose equal elements had\n * already been reordered, which is how a stable sort quietly stops being one.\n */\n\n/**\n * Whether this engine's `Array.prototype.sort` already exploits ordered input,\n * measured rather than guessed from the user agent.\n *\n * Sorting an already-sorted array of `PROBE_SIZE` elements takes exactly\n * `n - 1` comparisons on an adaptive sort and several times that otherwise.\n * Measured at `n = 128`: V8 does 127, Hermes does 741. The threshold below sits\n * between them with a wide margin at every size either engine was tried at.\n *\n * This exists so `sortByNaturalRuns` can be skipped where it cannot win. On V8\n * the native sort is already optimal on ordered input, so run detection only\n * adds a JS pass over data a faster native pass would have walked anyway --\n * measured at a 6% regression on cold-start hydration when applied there\n * unconditionally.\n *\n * The probe runs once, costs a few hundred comparisons, and is deliberately not\n * consulted by `sortByNaturalRuns` itself: callers decide, so the sort stays a\n * pure function that can be tested on whichever engine the tests run on.\n *\n * ## Why this is measured at runtime rather than compiled away\n *\n * The obvious alternative is a build-time flag, so that only the React Native\n * build carries this file at all -- 1,064 bytes minified, 590 gzipped. It was\n * considered and rejected, because \"React Native\" is a proxy for \"Hermes\" and\n * is wrong in both directions: React Native can run on JavaScriptCore, as it\n * did by default for years, and Hermes is embedded outside React Native. A\n * build keyed on the former would run the natural-run sort on an engine whose\n * own sort is already adaptive, and doing that is not merely wasted work: it\n * measured a 6.4% regression on cold-start hydration. The probe itself is not\n * what costs anything -- a few hundred comparisons, once: 127 where the native\n * sort is adaptive, 741 on Hermes, which is the one that pays most and the one\n * that gets the 17x back.\n *\n * Measured, sorting 128 already-sorted elements: V8 and JavaScriptCore each do\n * exactly `n - 1` comparisons, Hermes 741. SpiderMonkey was not measured, and\n * that is the argument rather than a hole in it. A hand-maintained list of\n * engines would have to be right about SpiderMonkey, about whatever ships next,\n * and about every embedding of each -- and would be wrong silently. The probe\n * is right about all of them without being told: if SpiderMonkey turns out not\n * to be adaptive, Firefox gets the faster path automatically, and if it is, it\n * never pays for one it does not need.\n */\nconst PROBE_SIZE = 128;\n\nlet adaptive: boolean | undefined;\n\nexport function nativeSortIsAdaptive(): boolean {\n if (adaptive === undefined) {\n const probe: number[] = [];\n for (let i = 0; i < PROBE_SIZE; i++) {\n probe.push(i);\n }\n let comparisons = 0;\n probe.sort((a, b) => {\n comparisons++;\n return a - b;\n });\n adaptive = comparisons < PROBE_SIZE * 2;\n }\n return adaptive;\n}\n\n/**\n * The default run budget for an array of `n` elements: `sqrt(n)`, floored at\n * 32. Integral, because the scan compares its run count against it for\n * equality.\n */\nfunction maxRunsFor(n: number): number {\n return Math.max(32, Math.floor(Math.sqrt(n)));\n}\n\n/**\n * Sorts `array` in place by merging its natural runs, and returns it.\n *\n * Returns `undefined`, leaving `array` **unmodified**, when it holds more than\n * `maxRuns` runs -- the caller should then sort it however it otherwise would.\n * `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see \"Where the\n * bound sits\" above.\n *\n * `compare` must be a consistent comparator; results are stable.\n */\nexport function sortByNaturalRuns<T>(\n array: T[],\n compare: (a: T, b: T) => number,\n maxRuns = maxRunsFor(array.length),\n): T[] | undefined {\n const n = array.length;\n if (n < 2) {\n return array;\n }\n\n // Pass one, which must not mutate: where each run starts, and whether it is\n // descending and so needs reversing before it can be merged.\n const starts: number[] = [];\n const descending: boolean[] = [];\n let i = 0;\n while (i < n) {\n // Stop before walking another run once `maxRuns` are already in hand and\n // input is left: this call cannot succeed now, and the next run could be\n // the whole remainder of the array.\n if (starts.length >= maxRuns) {\n return undefined;\n }\n const start = i;\n let isDescending = false;\n if (i + 1 < n) {\n const first = compare(array[i + 1], array[i]);\n // `c !== c` is a NaN test without a call. A comparator that returns NaN\n // is not an ordering -- it is not even antisymmetric -- so runs and\n // merges are meaningless and, worse, would silently disagree with the\n // caller's own sort: `compareValues` yields NaN for a NaN sort key, and\n // for the values `[1, NaN, 0]` a native sort leaves them alone while\n // merging produces `[0, 1, NaN]`. Decline and let the caller's sort\n // decide, so whatever it does today it keeps doing.\n //\n // This catches a NaN only where this pass happens to compare the key\n // holding it. A multi-column comparator short-circuits on the first\n // differing column, so a NaN in a later column stays invisible here and\n // first surfaces during the merge, which compares rows this pass never\n // put side by side. The merge checks too, and the merge path works on a\n // copy so that it can still decline when it does.\n if (first !== first) {\n return undefined;\n }\n isDescending = first < 0;\n i += 2;\n if (isDescending) {\n // Strictly descending: a run containing equal neighbours must not be\n // reversed, or their relative order would flip.\n while (i < n) {\n const c = compare(array[i], array[i - 1]);\n if (c !== c) {\n return undefined;\n }\n if (c >= 0) {\n break;\n }\n i++;\n }\n } else {\n while (i < n) {\n const c = compare(array[i], array[i - 1]);\n if (c !== c) {\n return undefined;\n }\n if (c < 0) {\n break;\n }\n i++;\n }\n }\n } else {\n i++;\n }\n starts.push(start);\n descending.push(isDescending);\n }\n starts.push(n);\n\n let runCount = descending.length;\n if (runCount === 1) {\n // One run means no merge, so no comparison beyond the ones already made:\n // nothing further can surface a NaN, and there is nothing to undo. This is\n // also the case worth the most -- already-sorted and exactly-reversed input\n // -- so it stays free of the copy the merge path needs.\n if (descending[0]) {\n reverse(array, 0, n - 1);\n }\n return array;\n }\n\n // From here the merge compares rows the counting pass never put side by side,\n // which is where a NaN in a later ordering column can first appear. Declining\n // at that point has to leave `array` exactly as it was, so the work happens on\n // a copy and is written back only once it has succeeded.\n let src = array.slice();\n // Pre-sized on purpose: it is filled by index below, and `Array.from` would\n // allocate and then fill it twice.\n // oxlint-disable-next-line unicorn/no-new-array\n let dst = new Array<T>(n);\n let bounds = starts;\n\n for (let r = 0; r < runCount; r++) {\n if (descending[r]) {\n reverse(src, starts[r], starts[r + 1] - 1);\n }\n }\n while (runCount > 1) {\n const nextBounds: number[] = [0];\n let write = 0;\n let merged = 0;\n for (let p = 0; p < runCount; p += 2) {\n let a = bounds[p];\n const endA = bounds[p + 1];\n if (p + 1 >= runCount) {\n while (a < endA) {\n dst[write++] = src[a++];\n }\n } else {\n let b = bounds[p + 1];\n const endB = bounds[p + 2];\n while (a < endA && b < endB) {\n const c = compare(src[b], src[a]);\n if (c !== c) {\n // A NaN the counting pass could not see. `array` has not been\n // written, so handing it back untouched is still possible.\n return undefined;\n }\n // `< 0` and not `<= 0`: on a tie the earlier run wins, which is what\n // keeps this stable.\n dst[write++] = c < 0 ? src[b++] : src[a++];\n }\n while (a < endA) {\n dst[write++] = src[a++];\n }\n while (b < endB) {\n dst[write++] = src[b++];\n }\n }\n merged++;\n nextBounds.push(write);\n }\n const swap = src;\n src = dst;\n dst = swap;\n bounds = nextBounds;\n runCount = merged;\n }\n\n for (let c = 0; c < n; c++) {\n array[c] = src[c];\n }\n return array;\n}\n\nfunction reverse<T>(array: T[], lo: number, hi: number): void {\n while (lo < hi) {\n const t = array[lo];\n array[lo++] = array[hi];\n array[hi--] = t;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6NA,IAAM,aAAa;AAEnB,IAAI;AAEJ,SAAgB,uBAAgC;CAC9C,IAAI,aAAa,KAAA,GAAW;EAC1B,MAAM,QAAkB,CAAC;EACzB,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAC9B,MAAM,KAAK,CAAC;EAEd,IAAI,cAAc;EAClB,MAAM,MAAM,GAAG,MAAM;GACnB;GACA,OAAO,IAAI;EACb,CAAC;EACD,WAAW,cAAc;CAC3B;CACA,OAAO;AACT;;;;;;AAOA,SAAS,WAAW,GAAmB;CACrC,OAAO,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC;AAC9C;;;;;;;;;;;AAYA,SAAgB,kBACd,OACA,SACA,UAAU,WAAW,MAAM,MAAM,GAChB;CACjB,MAAM,IAAI,MAAM;CAChB,IAAI,IAAI,GACN,OAAO;CAKT,MAAM,SAAmB,CAAC;CAC1B,MAAM,aAAwB,CAAC;CAC/B,IAAI,IAAI;CACR,OAAO,IAAI,GAAG;EAIZ,IAAI,OAAO,UAAU,SACnB;EAEF,MAAM,QAAQ;EACd,IAAI,eAAe;EACnB,IAAI,IAAI,IAAI,GAAG;GACb,MAAM,QAAQ,QAAQ,MAAM,IAAI,IAAI,MAAM,EAAE;GAe5C,IAAI,UAAU,OACZ;GAEF,eAAe,QAAQ;GACvB,KAAK;GACL,IAAI,cAGF,OAAO,IAAI,GAAG;IACZ,MAAM,IAAI,QAAQ,MAAM,IAAI,MAAM,IAAI,EAAE;IACxC,IAAI,MAAM,GACR;IAEF,IAAI,KAAK,GACP;IAEF;GACF;QAEA,OAAO,IAAI,GAAG;IACZ,MAAM,IAAI,QAAQ,MAAM,IAAI,MAAM,IAAI,EAAE;IACxC,IAAI,MAAM,GACR;IAEF,IAAI,IAAI,GACN;IAEF;GACF;EAEJ,OACE;EAEF,OAAO,KAAK,KAAK;EACjB,WAAW,KAAK,YAAY;CAC9B;CACA,OAAO,KAAK,CAAC;CAEb,IAAI,WAAW,WAAW;CAC1B,IAAI,aAAa,GAAG;EAKlB,IAAI,WAAW,IACb,QAAQ,OAAO,GAAG,IAAI,CAAC;EAEzB,OAAO;CACT;CAMA,IAAI,MAAM,MAAM,MAAM;CAItB,IAAI,MAAM,IAAI,MAAS,CAAC;CACxB,IAAI,SAAS;CAEb,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAC5B,IAAI,WAAW,IACb,QAAQ,KAAK,OAAO,IAAI,OAAO,IAAI,KAAK,CAAC;CAG7C,OAAO,WAAW,GAAG;EACnB,MAAM,aAAuB,CAAC,CAAC;EAC/B,IAAI,QAAQ;EACZ,IAAI,SAAS;EACb,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,GAAG;GACpC,IAAI,IAAI,OAAO;GACf,MAAM,OAAO,OAAO,IAAI;GACxB,IAAI,IAAI,KAAK,UACX,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;QAEhB;IACL,IAAI,IAAI,OAAO,IAAI;IACnB,MAAM,OAAO,OAAO,IAAI;IACxB,OAAO,IAAI,QAAQ,IAAI,MAAM;KAC3B,MAAM,IAAI,QAAQ,IAAI,IAAI,IAAI,EAAE;KAChC,IAAI,MAAM,GAGR;KAIF,IAAI,WAAW,IAAI,IAAI,IAAI,OAAO,IAAI;IACxC;IACA,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;IAErB,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;GAEvB;GACA;GACA,WAAW,KAAK,KAAK;EACvB;EACA,MAAM,OAAO;EACb,MAAM;EACN,MAAM;EACN,SAAS;EACT,WAAW;CACb;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,MAAM,KAAK,IAAI;CAEjB,OAAO;AACT;AAEA,SAAS,QAAW,OAAY,IAAY,IAAkB;CAC5D,OAAO,KAAK,IAAI;EACd,MAAM,IAAI,MAAM;EAChB,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ;CAChB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random-uint64.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-uint64.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"random-uint64.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-uint64.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,IAAI,MAAM,CAKrC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { getRandomValues } from "./random-values.js";
|
|
1
2
|
//#region ../shared/src/random-uint64.ts
|
|
2
3
|
function randomUint64() {
|
|
3
|
-
const high =
|
|
4
|
-
const low = Math.floor(Math.random() * 4294967295);
|
|
4
|
+
const [high, low] = getRandomValues(/* @__PURE__ */ new Uint32Array(2));
|
|
5
5
|
return BigInt(high) << 32n | BigInt(low);
|
|
6
6
|
}
|
|
7
7
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random-uint64.js","names":[],"sources":["../../../../shared/src/random-uint64.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"random-uint64.js","names":[],"sources":["../../../../shared/src/random-uint64.ts"],"sourcesContent":["import {getRandomValues} from './random-values.ts';\n\nexport function randomUint64(): bigint {\n // Two random 32-bit words rather than a BigUint64Array, which Hermes did\n // not have as of September 2024. The result is the same either way.\n const [high, low] = getRandomValues(new Uint32Array(2));\n return (BigInt(high) << 32n) | BigInt(low);\n}\n"],"mappings":";;AAEA,SAAgB,eAAuB;CAGrC,MAAM,CAAC,MAAM,OAAO,gCAAgB,IAAI,YAAY,CAAC,CAAC;CACtD,OAAQ,OAAO,IAAI,KAAK,MAAO,OAAO,GAAG;AAC3C"}
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
type RandomArray = Uint8Array<ArrayBuffer> | Uint32Array<ArrayBuffer>;
|
|
2
|
+
/**
|
|
3
|
+
* Fills `array` with random values. Uses `crypto.getRandomValues` when the
|
|
4
|
+
* runtime provides it, then the native `ExpoCrypto` module when running in an
|
|
5
|
+
* Expo app, and falls back to `Math.random` otherwise, logging a warning the
|
|
6
|
+
* first time the fallback is used.
|
|
7
|
+
*
|
|
8
|
+
* React Native's Hermes engine has no WebCrypto. Expo installs its native
|
|
9
|
+
* modules on `globalThis.expo.modules`, and `ExpoCrypto` is there in Expo Go
|
|
10
|
+
* and in any app that has `expo-crypto` installed, even if the app never
|
|
11
|
+
* imports it. Other React Native apps should import a polyfill such as
|
|
12
|
+
* `react-native-get-random-values` before creating IDs.
|
|
13
|
+
*
|
|
14
|
+
* The globals are looked up on every call rather than at module load: some
|
|
15
|
+
* runtimes (e.g. Cloudflare Workers) only allow generating random values
|
|
16
|
+
* while handling a request.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getRandomValues<T extends RandomArray>(array: T): T;
|
|
2
19
|
export declare function randomCharacters(length: number): string;
|
|
20
|
+
export {};
|
|
3
21
|
//# sourceMappingURL=random-values.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random-values.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-values.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"random-values.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-values.ts"],"names":[],"mappings":"AAEA,KAAK,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;AAgBtE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAuBlE;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,UAW9C"}
|