Files
rea/tests/boundary/providers/hopper/hopperStringSearch.test.ts
T
keemsisi 9426c3f1bb perf(hopper): resolve string search records once per query (#1229)
* perf(hopper): resolve string search records once per query

search_strings called _string_inventory(document) once per match, and each
call sorts every string address and rebuilds the record dict, so a query cost
O(matches x strings log strings). Resolve the inventory once per search.
Warm search, 50% matches, fake document, best of 7: 1000 strings 186 -> 0.5 ms,
4000 strings 3161 -> 2.1 ms, 8000 strings 13121 -> 4.9 ms; output byte-identical.

* perf(hopper): skip record resolution for empty string searches

Resolving the full string inventory for a query with no matches added a sort
of every string address that the previous code never performed. Return early
when nothing matches. Zero-match search on a warm document, best of 7:
64000 strings 35.9 ms before this change, 4.0 ms after (main: 3.9 ms).
2026-10-09 18:39:19 +08:00

81 lines
2.4 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { z } from "zod";
const execute = promisify(execFile);
const bridge = fileURLToPath(
new URL("../../../../bridge/hopper_bridge.py", import.meta.url),
);
const probe = fileURLToPath(
new URL("../../../fixtures/hopperStringSearchProbe.py", import.meta.url),
);
const resultSchema = z.array(
z.object({
pattern: z.string(),
case_sensitive: z.boolean(),
actual: z.array(z.object({ address: z.string() }).passthrough()),
expected: z.array(z.object({ address: z.string() }).passthrough()),
}),
);
async function runProbe() {
const { stdout } = await execute("python3", [probe, bridge], {
encoding: "utf8",
timeout: 5_000,
maxBuffer: 1_024 * 1_024,
});
return resultSchema.parse(JSON.parse(stdout));
}
describe("Hopper string search", () => {
it("returns the independently expected records for every query, including repeats", async () => {
const results = await runProbe();
expect(results).toHaveLength(14);
for (const result of results) {
expect(result.actual).toEqual(result.expected);
}
});
it("matches case-insensitively and case-sensitively across segments in ascending address order", async () => {
const results = await runProbe();
const byQuery = (pattern: string, caseSensitive: boolean) =>
results
.find(
(item) =>
item.pattern === pattern && item.case_sensitive === caseSensitive,
)
?.actual.map((item) => item.address);
expect(byQuery("needle", false)).toEqual([
"0x1010",
"0x1030",
"0x1050",
"0x2000",
"0x3020",
"0x3040",
]);
expect(byQuery("needle", true)).toEqual(["0x1010", "0x2000", "0x3040"]);
expect(byQuery("zzz-absent", false)).toEqual([]);
});
it("preserves the decoding-unavailable record for an unreadable string", async () => {
const results = await runProbe();
const search = results.find(
(item) => item.pattern === "Broken" && item.case_sensitive,
);
expect(search?.actual).toEqual([
{
address: "0x1030",
value: "Broken Needle",
provider_value: "Broken Needle",
decoding: {
available: false,
reason: "Hopper reported an invalid string object extent at 0x1030",
},
},
]);
});
});