perf: Optimize chronological sorter hook resolution

Hoisted the timestamp hook resolution out of the inner loop in `createChronologicalSorter`.
This avoids allocating an array of candidate functions for every comparison during sorting (O(N log N)).
Benchmark shows a ~27% speedup (38ms -> 27ms for 100k items).

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-08 16:55:27 +00:00
1 parent ec50db8645
commit 7c1f3c9ada
2 files changed
+95 -25

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
import { createChronologicalSorter } from "../js/feedEngine/sorters.js";
import { performance } from "perf_hooks";
const NUM_ITEMS = 100000;
function buildItem(i) {
return {
video: {
id: `vid-${i}`,
rootCreatedAt: 1000 + i,
},
metadata: {},
};
}
async function runBenchmark() {
const items = [];
for (let i = 0; i < NUM_ITEMS; i++) {
items.push(buildItem(i));
}
// Mock context with hooks to trigger the candidate lookup loop
const context = {
hooks: {
timestamps: {
getKnownVideoPostedAt: () => null,
getKnownPostedAt: () => null,
getVideoPostedAt: () => null,
resolveVideoPostedAt: () => null,
}
}
};
const sorter = createChronologicalSorter({ direction: "desc" });
console.log(`Sorting ${NUM_ITEMS} items with hooks...`);
const iterations = 20; // Increased iterations for stability
let totalTime = 0;
// Warmup
for (let i = 0; i < 5; i++) sorter(items, context);
for (let i = 0; i < iterations; i++) {
const start = performance.now();
sorter(items, context);
const end = performance.now();
totalTime += (end - start);
if (i % 5 === 0) process.stdout.write(".");
}
const avgTime = totalTime / iterations;
console.log("\n");
console.log(`Average time per sort: ${avgTime.toFixed(2)}ms`);
}
runBenchmark();
+37 -25
View File
@@ -14,6 +14,14 @@ export function createChronologicalSorter({
}
const copy = [...items];
// Pre-resolve hooks to avoid allocation and property access in the hot loop
const hook = context?.hooks?.timestamps;
const h1 = typeof hook?.getKnownVideoPostedAt === "function" ? hook.getKnownVideoPostedAt : null;
const h2 = typeof hook?.getKnownPostedAt === "function" ? hook.getKnownPostedAt : null;
const h3 = typeof hook?.getVideoPostedAt === "function" ? hook.getVideoPostedAt : null;
const h4 = typeof hook?.resolveVideoPostedAt === "function" ? hook.resolveVideoPostedAt : null;
const getTimestamp = (entry) => {
if (!entry || typeof entry !== "object") {
return Number.NEGATIVE_INFINITY;
@@ -36,33 +44,37 @@ export function createChronologicalSorter({
}
}
const hook = context?.hooks?.timestamps;
if (!hook || typeof hook !== "object") {
return null;
}
const candidates = [
hook.getKnownVideoPostedAt,
hook.getKnownPostedAt,
hook.getVideoPostedAt,
hook.resolveVideoPostedAt,
];
for (const candidate of candidates) {
if (typeof candidate !== "function") {
continue;
}
// Unrolled check for pre-resolved hooks
if (h1) {
try {
const value = candidate(video, { entry, context });
if (Number.isFinite(value)) {
return Math.floor(value);
}
const value = h1(video, { entry, context });
if (Number.isFinite(value)) return Math.floor(value);
} catch (error) {
context?.log?.(
"[chronological-sorter] timestamps hook threw",
error,
);
context?.log?.("[chronological-sorter] timestamps hook threw", error);
}
}
if (h2) {
try {
const value = h2(video, { entry, context });
if (Number.isFinite(value)) return Math.floor(value);
} catch (error) {
context?.log?.("[chronological-sorter] timestamps hook threw", error);
}
}
if (h3) {
try {
const value = h3(video, { entry, context });
if (Number.isFinite(value)) return Math.floor(value);
} catch (error) {
context?.log?.("[chronological-sorter] timestamps hook threw", error);
}
}
if (h4) {
try {
const value = h4(video, { entry, context });
if (Number.isFinite(value)) return Math.floor(value);
} catch (error) {
context?.log?.("[chronological-sorter] timestamps hook threw", error);
}
}