diff --git a/packages/zql/src/ivm/data.ts b/packages/zql/src/ivm/data.ts index b73acced02..1c0c2892aa 100644 --- a/packages/zql/src/ivm/data.ts +++ b/packages/zql/src/ivm/data.ts @@ -89,11 +89,24 @@ export function normalizeUndefined(v: Value): NormalizedValue { export type Comparator = (r1: Row, r2: Row) => number; export function makeComparator(order: Ordering, reverse?: boolean): Comparator { + // A single ascending field is the common shape -- it is what every source + // ordered by a single-column primary key gets -- and specializing it drops + // the loop, the direction test and the reverse test from the hottest + // function in hydration. + if (order.length === 1 && order[0][1] === 'asc' && !reverse) { + const field = order[0][0]; + return (a, b) => compareValues(a[field], b[field]); + } + + const length = order.length; return (a, b) => { - // Skip destructuring here since it is hot code. - for (const ord of order) { - const field = ord[0]; - const comp = compareValues(a[field], b[field]); + // Skip destructuring here since it is hot code. An indexed loop rather + // than `for...of` for the same reason: Hermes allocates an iterator and + // calls `next()` per element, which costs more than the comparison for an + // ordering this short. + for (let i = 0; i < length; i++) { + const ord = order[i]; + const comp = compareValues(a[ord[0]], b[ord[0]]); if (comp !== 0) { const result = ord[1] === 'asc' ? comp : -comp; return reverse ? -result : result; diff --git a/packages/zql/src/ivm/view-apply-change.ts b/packages/zql/src/ivm/view-apply-change.ts index 2323f94b0f..62bb716cbf 100644 --- a/packages/zql/src/ivm/view-apply-change.ts +++ b/packages/zql/src/ivm/view-apply-change.ts @@ -768,11 +768,31 @@ function binarySearch( target: Row, comparator: Comparator, ): number { - let low = 0; let high = view.length - 1; + if (high < 0) { + return ~0; + } + + // Probe the last entry before searching. Hydration feeds the view rows in + // the query's sort order, so every insert belongs at the end and the plain + // search spends log2(n) comparisons to rediscover that -- about eleven per + // row for a view of a couple of thousand. Row comparison is the single + // largest cost in hydration on Hermes, so collapsing those eleven to one is + // worth the one extra comparison this costs when the row does land inside + // the view, which is a single push rather than a bulk load. + // MetaEntry has all Row props; comparator only reads string keys + const last = comparator(view[high] as Row, target); + if (last < 0) { + return ~(high + 1); + } + if (last === 0) { + return high; + } + + let low = 0; + high -= 1; while (low <= high) { const mid = (low + high) >>> 1; - // MetaEntry has all Row props; comparator only reads string keys const comparison = comparator(view[mid] as Row, target); if (comparison < 0) { low = mid + 1;