summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig16
-rw-r--r--docs/lsp.md2
-rw-r--r--test/bench_util.zig43
-rw-r--r--test/lspbench.zig169
-rw-r--r--test/pdf_bench.zig23
-rw-r--r--test/pdf_pointer_bench.zig14
-rw-r--r--test/pdf_scroll_bench.zig43
-rw-r--r--test/pdf_sections_bench.zig65
-rw-r--r--tools/lspprobe.zig184
9 files changed, 251 insertions, 308 deletions
diff --git a/build.zig b/build.zig
index fcb4b0b7..eef05b44 100644
--- a/build.zig
+++ b/build.zig
@@ -1370,7 +1370,7 @@ pub fn build(b: *std.Build) void {
if (b.args) |args| run_lspbench.addArgs(args);
run_lspbench.setCwd(b.path("."));
run_lspbench.has_side_effects = true;
- const lspbench_step = b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [--check] [repo-root])");
+ const lspbench_step = b.step("lspbench", "language-backend latency + feature matrix (-- [--json] [--check] [repo-root]), or one query (-- probe <kind> <file> <line>:<col> [arg] [--reps N])");
lspbench_step.dependOn(&run_lspbench.step);
const run_lspbench_check = b.addRunArtifact(lspbench);
run_lspbench_check.addArg("--check");
@@ -1380,20 +1380,6 @@ pub fn build(b: *std.Build) void {
const lspbench_check_step = b.step("lspbench-check", "require every configured language probe to complete correctly");
lspbench_check_step.dependOn(&run_lspbench_check.step);
- const lspprobe = b.addExecutable(.{
- .name = "pardes-lspprobe",
- .root_module = b.createModule(.{
- .target = target,
- .optimize = optimize,
- .root_source_file = b.path("tools/lspprobe.zig"),
- .link_libc = true,
- }),
- });
- lspprobe.root_module.addImport("pardes", core_module);
- const run_lspprobe = b.addRunArtifact(lspprobe);
- if (b.args) |args| run_lspprobe.addArgs(args);
- const lspprobe_step = b.step("lspprobe", "one language query against the real seam (-- <kind> <file> <line>:<col> [arg] [--reps N])");
- lspprobe_step.dependOn(&run_lspprobe.step);
const locations_bench_step = b.step("locations-bench", "measure cold/warm result formatting (-Doptimize=ReleaseFast; CSV on stderr)");
if (optimize != .ReleaseFast) {
diff --git a/docs/lsp.md b/docs/lsp.md
index ae350156..22cf1b90 100644
--- a/docs/lsp.md
+++ b/docs/lsp.md
@@ -51,7 +51,7 @@ position encoding, and handles both push and pull diagnostics. Startup and
request waits have deadlines; failed starts back off before retrying. Worker
status messages reach the host event queue.
-`zig build lspprobe -- gd <file> <line>:<column>` queries the same backend from
+`zig build lspbench -- probe gd <file> <line>:<column>` queries the same backend from
the command line. `zig build lspbench` measures it. The Zig snapshot tests
(lsp, lspcomplete, lspdebug and the rest) run the real `zls`; the protocol
ones use a Zig mock server with deterministic answers while exercising the real client,
diff --git a/test/bench_util.zig b/test/bench_util.zig
new file mode 100644
index 00000000..9b91e621
--- /dev/null
+++ b/test/bench_util.zig
@@ -0,0 +1,43 @@
+//! What the PDF benches share: a clock, a fatal exit named for the bench, a
+//! seeded hash, a count parser and the core's effect drain.
+const std = @import("std");
+
+pub fn nowNs() u64 {
+ var ts: std.c.timespec = undefined;
+ _ = std.c.clock_gettime(.MONOTONIC, &ts);
+ return @as(u64, @intCast(ts.sec)) *| 1_000_000_000 +| @as(u64, @intCast(ts.nsec));
+}
+
+/// `fatal` for the bench called `who`: says so, prefixed, and exits 1.
+pub fn Fatal(comptime who: []const u8) type {
+ return struct {
+ pub fn f(comptime format: []const u8, args: anytype) noreturn {
+ std.debug.print(who ++ ": " ++ format ++ "\n", args);
+ std.process.exit(1);
+ }
+ };
+}
+
+/// A seeded hash of `value`'s bytes; a comptime integer hashes as a u64.
+pub fn mix(seed: u64, value: anytype) u64 {
+ const Value = @TypeOf(value);
+ const Stable = switch (@typeInfo(Value)) {
+ .comptime_int => u64,
+ else => Value,
+ };
+ var stable: Stable = value;
+ return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
+}
+
+/// A flag's value as a count in 1..`maximum`, or `fatal` saying why not.
+pub fn parsePositive(comptime fatal: anytype, name: []const u8, text: []const u8, maximum: usize) usize {
+ const value = std.fmt.parseUnsigned(usize, text, 10) catch
+ fatal("{s} expects a positive integer", .{name});
+ if (value == 0 or value > maximum)
+ fatal("{s} must be in 1..{d}", .{ name, maximum });
+ return value;
+}
+
+pub fn drainEffects(core: anytype) void {
+ while (core.nextEffect()) |_| {}
+}
diff --git a/test/lspbench.zig b/test/lspbench.zig
index e094ed69..02138d24 100644
--- a/test/lspbench.zig
+++ b/test/lspbench.zig
@@ -114,6 +114,7 @@ pub fn main(init: std.process.Init) !void {
const gpa = std.heap.page_allocator;
const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len > 1 and std.mem.eql(u8, args[1], "probe")) return probeCommand(init);
const options = try Options.parse(args[1..]);
const rss0 = rssKib();
@@ -295,3 +296,171 @@ fn rssKib() u64 {
}
return 0;
}
+
+// ---- probe: one query from the command line ----
+
+fn nowUs() u64 {
+ var ts: std.c.timespec = undefined;
+ _ = std.c.clock_gettime(.MONOTONIC, &ts);
+ return @as(u64, @intCast(ts.sec)) *| 1_000_000 +| @as(u64, @intCast(ts.nsec)) / 1000;
+}
+
+const aliases = .{
+ .{ "gd", lsp.Kind.definition },
+ .{ "gD", lsp.Kind.declaration },
+ .{ "gy", lsp.Kind.type_definition },
+ .{ "gi", lsp.Kind.implementation },
+ .{ "gr", lsp.Kind.references },
+};
+
+fn probeUsage() noreturn {
+ std.debug.print(
+ \\usage: pardes-lspbench probe <kind> <file> <line>:<col> [arg] [--reps N] [--warm S]
+ \\ kind: gd gD gy gi gr, or any lsp.Kind tag (hover, references,
+ \\ document_symbols, workspace_symbols, incoming_calls, ...)
+ \\ arg: the new name for rename, the query for workspace_symbols
+ \\ --warm sleep S seconds after the first (cold) query — long enough
+ \\ for an indexing server to reach ready, narrated on stderr
+ \\
+ , .{});
+ std.process.exit(2);
+}
+
+/// The editor's message row, here: the client's reader threads narrate server
+/// state through the same sink the shells register, printed to stderr so the
+/// rows on stdout stay clean.
+fn statusSink(ctx: ?*anyopaque, text: []const u8) void {
+ _ = ctx;
+ std.debug.print("# status: {s}\n", .{text});
+}
+
+extern "c" fn usleep(usec: c_uint) c_int;
+
+/// `lspbench probe`: one language query against the real seam, from the
+/// command line (the old lspprobe): `probe <kind> <file> <line>:<col> [arg]
+/// [--reps N] [--warm S]`, rows on stdout, the timing on stderr.
+fn probeCommand(init: std.process.Init) !void {
+ const gpa = std.heap.page_allocator;
+ const arena = init.arena.allocator();
+ const io = std.Io.Threaded.global_single_threaded.io();
+
+ // args[1] is `probe`: the rest are where lspprobe's were.
+ const all = try init.minimal.args.toSlice(arena);
+ const args = all[1..];
+ if (args.len < 4) probeUsage();
+
+ // comma-separated kinds share ONE server instance and its index — the
+ // whole battery against a big workspace pays the cold cost once
+ var kinds: [16]lsp.Kind = undefined;
+ var nkinds: usize = 0;
+ var kit = std.mem.tokenizeScalar(u8, args[1], ',');
+ while (kit.next()) |word| {
+ if (nkinds == kinds.len) probeUsage();
+ kinds[nkinds] = blk: {
+ inline for (aliases) |a| if (std.mem.eql(u8, word, a[0])) break :blk a[1];
+ break :blk std.meta.stringToEnum(lsp.Kind, word) orelse probeUsage();
+ };
+ nkinds += 1;
+ }
+ if (nkinds == 0) probeUsage();
+
+ var pz: [4096:0]u8 = undefined;
+ const pz_s = std.fmt.bufPrintSentinel(&pz, "{s}", .{args[2]}, 0) catch probeUsage();
+ var realbuf: [4096]u8 = undefined;
+ const path = std.mem.span(realpath(pz_s.ptr, &realbuf) orelse {
+ std.debug.print("cannot resolve {s}\n", .{args[2]});
+ std.process.exit(1);
+ });
+ const source = std.Io.Dir.cwd().readFileAllocOptions(
+ io,
+ path,
+ arena,
+ .limited(64 << 20),
+ .of(u8),
+ 0,
+ ) catch {
+ std.debug.print("cannot read {s}\n", .{path});
+ std.process.exit(1);
+ };
+
+ const colon = std.mem.indexOfScalar(u8, args[3], ':') orelse probeUsage();
+ const line = std.fmt.parseInt(u32, args[3][0..colon], 10) catch probeUsage();
+ const col = std.fmt.parseInt(u32, args[3][colon + 1 ..], 10) catch probeUsage();
+ if (line == 0 or col == 0) probeUsage();
+
+ var arg: []const u8 = "";
+ var reps: u32 = 1;
+ var warm_s: u32 = 0;
+ var i: usize = 4;
+ while (i < args.len) : (i += 1) {
+ if (std.mem.eql(u8, args[i], "--reps")) {
+ i += 1;
+ if (i >= args.len) probeUsage();
+ reps = @max(1, std.fmt.parseInt(u32, args[i], 10) catch probeUsage());
+ } else if (std.mem.eql(u8, args[i], "--warm")) {
+ i += 1;
+ if (i >= args.len) probeUsage();
+ warm_s = std.fmt.parseInt(u32, args[i], 10) catch probeUsage();
+ } else arg = args[i];
+ }
+
+ lsp.setStatusSink(null, statusSink);
+
+ // 1-based line:col -> byte offset
+ var off: usize = 0;
+ var ln: u32 = 1;
+ while (ln < line and off < source.len) : (off += 1) {
+ if (source[off] == '\n') ln += 1;
+ }
+ off = @min(off + col - 1, source.len);
+
+ const root = std.fs.path.dirname(path) orelse "/";
+ const timings = try arena.alloc(u64, reps);
+ var stdout_buf: [64 * 1024]u8 = undefined;
+ var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
+ var first_query = true;
+
+ for (kinds[0..nkinds]) |kind| {
+ var rows: []const u8 = "";
+ for (0..reps) |r| {
+ // one warm pause for the whole battery, right after the very
+ // first (cold, server-spawning) query — the status lines on
+ // stderr narrate what the server does with the time
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ var scratch: std.heap.ArenaAllocator = .init(gpa);
+ defer scratch.deinit();
+ const t0 = nowUs();
+ try lsp.query(gpa, scratch.allocator(), .{
+ .kind = kind,
+ .path = path,
+ .source = source,
+ .offset = @intCast(off),
+ .arg = arg,
+ .root = root,
+ }, &out.writer);
+ timings[r] = nowUs() -| t0;
+ rows = try arena.dupe(u8, out.written());
+ if (first_query) {
+ first_query = false;
+ if (warm_s > 0) for (0..warm_s * 10) |_| {
+ _ = usleep(100_000);
+ };
+ }
+ }
+
+ try stdout.interface.print("## {t}\n", .{kind});
+ try stdout.interface.writeAll(rows);
+ try stdout.interface.flush();
+
+ const n = std.mem.count(u8, rows, "\n");
+ const cold = timings[0];
+ std.mem.sort(u64, timings[0..reps], {}, std.sort.asc(u64));
+ if (reps == 1)
+ std.debug.print("# {t}: {d} row(s) in {d}us\n", .{ kind, n, cold })
+ else
+ std.debug.print("# {t}: {d} row(s); first {d}us, min {d}us, median {d}us ({d} reps)\n", .{
+ kind, n, cold, timings[0], timings[reps / 2], reps,
+ });
+ }
+}
diff --git a/test/pdf_bench.zig b/test/pdf_bench.zig
index 0f09f72a..23565106 100644
--- a/test/pdf_bench.zig
+++ b/test/pdf_bench.zig
@@ -13,6 +13,9 @@
//! filesystem cache is deliberately uncontrolled), while warm scenarios
//! retain one Document.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-bench").f;
const pdf = @import("mupdf");
pub const std_options: std.Options = .{ .log_level = .err };
@@ -205,14 +208,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parsePositive(name: []const u8, text: []const u8, maximum: usize) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{name});
- if (value == 0 or value > maximum)
- fatal("{s} must be in 1..{d}", .{ name, maximum });
- return value;
-}
-
fn usage() noreturn {
printUsage();
std.process.exit(2);
@@ -226,13 +221,6 @@ fn printUsage() void {
);
}
-fn nowNs() u64 {
- var ts: std.c.timespec = undefined;
- _ = std.c.clock_gettime(.MONOTONIC, &ts);
- return @as(u64, @intCast(ts.sec)) *| 1_000_000_000 +|
- @as(u64, @intCast(ts.nsec));
-}
-
fn measure(
config: Config,
profile_index: usize,
@@ -617,7 +605,6 @@ fn reportJson(
std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
+fn parsePositive(name: []const u8, text: []const u8, maximum: usize) usize {
+ return bench_util.parsePositive(fatal, name, text, maximum);
}
diff --git a/test/pdf_pointer_bench.zig b/test/pdf_pointer_bench.zig
index f1d61229..ae2de2f5 100644
--- a/test/pdf_pointer_bench.zig
+++ b/test/pdf_pointer_bench.zig
@@ -15,6 +15,9 @@
//! frame each step settles on. It is the no-regression proof: a change to how highlights
//! are painted may change what it costs, never one presented pixel.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const mix = bench_util.mix;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -208,17 +211,6 @@ fn print(comptime format: []const u8, args: anytype) void {
_ = std.c.write(1, line.ptr, line.len);
}
-fn mix(seed: u64, value: anytype) u64 {
- var copy = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&copy));
-}
-
-fn nowNs() u64 {
- var ts: std.c.timespec = undefined;
- _ = std.c.clock_gettime(.MONOTONIC, &ts);
- return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
-}
-
comptime {
_ = pdf.Document;
}
diff --git a/test/pdf_scroll_bench.zig b/test/pdf_scroll_bench.zig
index 33dfe70a..a4511dbe 100644
--- a/test/pdf_scroll_bench.zig
+++ b/test/pdf_scroll_bench.zig
@@ -42,6 +42,11 @@
//!
//! Setup, teardown, checksumming, and reporting are outside every timer.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-scroll-bench").f;
+const mix = bench_util.mix;
+const drainEffects = bench_util.drainEffects;
const libc = std.c;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -211,14 +216,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parseCount(flag: []const u8, text: []const u8) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{flag});
- if (value == 0 or value > 10_000)
- fatal("{s} must be in 1..10000", .{flag});
- return value;
-}
-
const Travel = struct {
/// Scrollable display pixels: the whole strip minus one screenful.
max_scroll: f64,
@@ -520,20 +517,6 @@ fn stridedHash(seed: u64, rgba: []const u8) u64 {
return hash;
}
-fn mix(seed: u64, value: anytype) u64 {
- const Value = @TypeOf(value);
- const Stable = switch (@typeInfo(Value)) {
- .comptime_int => u64,
- else => Value,
- };
- var stable: Stable = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
-}
-
-fn drainEffects(core: *pardes.Pardes) void {
- while (core.nextEffect()) |_| {}
-}
-
fn verifyIdentity(
name: []const u8,
expected: *?Identity,
@@ -557,13 +540,6 @@ fn verifyIdentity(
} else expected.* = got;
}
-fn nowNs() u64 {
- var ts: std.c.timespec = undefined;
- _ = std.c.clock_gettime(.MONOTONIC, &ts);
- return @as(u64, @intCast(ts.sec)) *| 1_000_000_000 +|
- @as(u64, @intCast(ts.nsec));
-}
-
fn reportText(config: Config, travel: Travel, results: []const Result) void {
std.debug.print("pardes PDF fast-scroll benchmark ({s})\n", .{bench_config.core_optimize});
std.debug.print(
@@ -649,13 +625,12 @@ fn reportJson(io: std.Io, config: Config, travel: Travel, results: []const Resul
std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-scroll-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
-}
-
comptime {
// The bench links the real MuPDF wrapper the core uses; keep the import
// load-bearing so a build that silently dropped it fails here.
_ = pdf.Document;
}
+
+fn parseCount(flag: []const u8, text: []const u8) usize {
+ return bench_util.parsePositive(fatal, flag, text, 10_000);
+}
diff --git a/test/pdf_sections_bench.zig b/test/pdf_sections_bench.zig
index b08af552..92c9ef7c 100644
--- a/test/pdf_sections_bench.zig
+++ b/test/pdf_sections_bench.zig
@@ -9,6 +9,11 @@
//! report nanoseconds per operation; `operations_per_sample` makes that
//! normalization explicit in both output formats.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-sections-bench").f;
+const mix = bench_util.mix;
+const drainEffects = bench_util.drainEffects;
const libc = std.c;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -107,14 +112,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parseCount(flag: []const u8, text: []const u8) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{flag});
- if (value == 0 or value > 10_000)
- fatal("{s} must be in 1..10000", .{flag});
- return value;
-}
-
fn measureOutline(path: []const u8, config: Config) !Result {
const samples = try gpa.alloc(u64, config.reps);
defer gpa.free(samples);
@@ -145,7 +142,7 @@ fn measureMaterialize(
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
- const content = try pardes.PdfSectionRows.render(gpa, path, entries);
+ const content = try pardes.panes.Pdf.SectionRows.render(gpa, path, entries);
const elapsed = nowNs() -| started;
const identity = contentIdentity(content);
gpa.free(content);
@@ -255,7 +252,7 @@ fn measureWorstShape(entries: []const pdf.OutlineEntry, config: Config) !Result
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
- const content = try pardes.PdfSectionRows.render(gpa, "/tmp/near-limit.pdf", entries);
+ const content = try pardes.panes.Pdf.SectionRows.render(gpa, "/tmp/near-limit.pdf", entries);
const elapsed = nowNs() -| started;
const identity = contentIdentity(content);
gpa.free(content);
@@ -300,11 +297,11 @@ fn perturbReopen(prepared: PreparedReopen, operation: usize) void {
const marker: i32 = @intCast(operation % 31 + 1);
const output_file = &prepared.output.file.?;
output_file.scroll = @intCast(operation % 17 + 1);
- prepared.output.cur_row = marker;
- prepared.output.cur_col = marker + 1;
- prepared.output.msel.active = true;
- prepared.output.vsel.active = true;
- prepared.output.nsel = 1;
+ prepared.output.body.cur_row = marker;
+ prepared.output.body.cur_col = marker + 1;
+ prepared.output.body.msel.active = true;
+ prepared.output.body.vsel.active = true;
+ prepared.output.body.nsel = 1;
prepared.owner.search_pane = null;
prepared.owner.search_row = operation % 23;
prepared.core.active = prepared.output_id;
@@ -313,21 +310,17 @@ fn perturbReopen(prepared: PreparedReopen, operation: usize) void {
fn observeReopen(prepared: PreparedReopen) u64 {
const file = prepared.output.file.?;
var observed: u64 = file.scroll;
- observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.cur_row)));
- observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.cur_col)));
- observed = observed *% 131 +% @intFromBool(prepared.output.msel.active);
- observed = observed *% 131 +% @intFromBool(prepared.output.vsel.active);
- observed = observed *% 131 +% prepared.output.nsel;
+ observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.body.cur_row)));
+ observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.body.cur_col)));
+ observed = observed *% 131 +% @intFromBool(prepared.output.body.msel.active);
+ observed = observed *% 131 +% @intFromBool(prepared.output.body.vsel.active);
+ observed = observed *% 131 +% prepared.output.body.nsel;
observed = observed *% 131 +% (prepared.owner.search_pane orelse std.math.maxInt(usize));
observed = observed *% 131 +% (prepared.owner.search_row orelse std.math.maxInt(usize));
observed = observed *% 131 +% prepared.core.active;
return observed;
}
-fn drainEffects(core: *pardes.Pardes) void {
- while (core.nextEffect()) |_| {}
-}
-
fn sectionsOutputIdentity(core: *pardes.Pardes) Identity {
const owner = core.panes[0] orelse fatal("PDF pane disappeared", .{});
const output_id = owner.search_pane orelse fatal("+PdfSections was not armed", .{});
@@ -347,7 +340,7 @@ fn stepIdentity(core: *pardes.Pardes) Identity {
identity.checksum = mix(identity.checksum, view.page);
identity.checksum = mix(identity.checksum, @as(u64, @bitCast(view.document_scroll_y)));
identity.checksum = mix(identity.checksum, owner.search_row orelse std.math.maxInt(usize));
- identity.checksum = mix(identity.checksum, view.render_revision);
+ identity.checksum = mix(identity.checksum, view.next_raster_revision);
return identity;
}
@@ -389,16 +382,6 @@ fn outlineIdentity(entries: []const pdf.OutlineEntry) Identity {
return .{ .items = entries.len, .bytes = bytes, .checksum = checksum };
}
-fn mix(seed: u64, value: anytype) u64 {
- const Value = @TypeOf(value);
- const Stable = switch (@typeInfo(Value)) {
- .comptime_int => u64,
- else => Value,
- };
- var stable: Stable = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
-}
-
fn makeWorstShape() ![]pdf.OutlineEntry {
const entries = try gpa.alloc(pdf.OutlineEntry, near_limit_items);
for (entries, 0..) |*entry, ordinal| {
@@ -457,13 +440,6 @@ fn summarize(
};
}
-fn nowNs() u64 {
- var ts: std.c.timespec = undefined;
- _ = std.c.clock_gettime(.MONOTONIC, &ts);
- return @as(u64, @intCast(ts.sec)) *| 1_000_000_000 +|
- @as(u64, @intCast(ts.nsec));
-}
-
/// A host benchmark compiler barrier. MuPDF benchmarks are build-and-run
/// steps, so they never target the freestanding web backend; an empty volatile
/// asm with only the abstract memory clobber is valid on every native backend
@@ -545,7 +521,6 @@ fn unlink(path: []const u8) void {
_ = libc.unlink(path_z);
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-sections-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
+fn parseCount(flag: []const u8, text: []const u8) usize {
+ return bench_util.parsePositive(fatal, flag, text, 10_000);
}
diff --git a/tools/lspprobe.zig b/tools/lspprobe.zig
deleted file mode 100644
index 0b7d772c..00000000
--- a/tools/lspprobe.zig
+++ /dev/null
@@ -1,184 +0,0 @@
-//! One language query against the REAL seam, from the command line.
-//!
-//! pardes-lspprobe <kind> <file> <line>:<col> [arg] [--reps N]
-//!
-//! `kind` is an `lsp.Kind` tag (`definition`, `references`, ...) or its helix
-//! key (`gd`, `gr`, `gy`, `gi`, `gD`). Line and column are 1-based, the way
-//! the rows print them. Rows go to stdout; the timing line goes to stderr so
-//! `lspprobe gd f.rs 10:5 | head` still composes.
-//!
-//! This is the development probe for the PROTOCOL CLIENT: point it at a file
-//! in any workspace (a rust crate, a C tree with compile_commands.json) and it
-//! exercises exactly what `gd` in the editor would — same seam, same client,
-//! same child server. The server is spawned fresh per invocation and kept for
-//! the process, so rep 0 is the true cold cost (spawn + handshake + whatever
-//! indexing the deadline allows) and later reps are the warm per-press cost.
-//! Generic on purpose: no project names anywhere — the workspace is whatever
-//! the file's root markers say.
-const std = @import("std");
-const lsp = @import("pardes").lsp;
-
-extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8;
-
-pub const std_options: std.Options = .{ .log_level = .err };
-
-fn nowUs() u64 {
- var ts: std.c.timespec = undefined;
- _ = std.c.clock_gettime(.MONOTONIC, &ts);
- return @as(u64, @intCast(ts.sec)) *| 1_000_000 +| @as(u64, @intCast(ts.nsec)) / 1000;
-}
-
-const aliases = .{
- .{ "gd", lsp.Kind.definition },
- .{ "gD", lsp.Kind.declaration },
- .{ "gy", lsp.Kind.type_definition },
- .{ "gi", lsp.Kind.implementation },
- .{ "gr", lsp.Kind.references },
-};
-
-fn usage() noreturn {
- std.debug.print(
- \\usage: pardes-lspprobe <kind> <file> <line>:<col> [arg] [--reps N] [--warm S]
- \\ kind: gd gD gy gi gr, or any lsp.Kind tag (hover, references,
- \\ document_symbols, workspace_symbols, incoming_calls, ...)
- \\ arg: the new name for rename, the query for workspace_symbols
- \\ --warm sleep S seconds after the first (cold) query — long enough
- \\ for an indexing server to reach ready, narrated on stderr
- \\
- , .{});
- std.process.exit(2);
-}
-
-/// The editor's message row, here: the client's reader threads narrate server
-/// state through the same sink the shells register, printed to stderr so the
-/// rows on stdout stay clean.
-fn statusSink(ctx: ?*anyopaque, text: []const u8) void {
- _ = ctx;
- std.debug.print("# status: {s}\n", .{text});
-}
-
-extern "c" fn usleep(usec: c_uint) c_int;
-
-pub fn main(init: std.process.Init) !void {
- const gpa = std.heap.page_allocator;
- const arena = init.arena.allocator();
- const io = std.Io.Threaded.global_single_threaded.io();
-
- const args = try init.minimal.args.toSlice(arena);
- if (args.len < 4) usage();
-
- // comma-separated kinds share ONE server instance and its index — the
- // whole battery against a big workspace pays the cold cost once
- var kinds: [16]lsp.Kind = undefined;
- var nkinds: usize = 0;
- var kit = std.mem.tokenizeScalar(u8, args[1], ',');
- while (kit.next()) |word| {
- if (nkinds == kinds.len) usage();
- kinds[nkinds] = blk: {
- inline for (aliases) |a| if (std.mem.eql(u8, word, a[0])) break :blk a[1];
- break :blk std.meta.stringToEnum(lsp.Kind, word) orelse usage();
- };
- nkinds += 1;
- }
- if (nkinds == 0) usage();
-
- var pz: [4096:0]u8 = undefined;
- const pz_s = std.fmt.bufPrintSentinel(&pz, "{s}", .{args[2]}, 0) catch usage();
- var realbuf: [4096]u8 = undefined;
- const path = std.mem.span(realpath(pz_s.ptr, &realbuf) orelse {
- std.debug.print("cannot resolve {s}\n", .{args[2]});
- std.process.exit(1);
- });
- const source = std.Io.Dir.cwd().readFileAllocOptions(
- io,
- path,
- arena,
- .limited(64 << 20),
- .of(u8),
- 0,
- ) catch {
- std.debug.print("cannot read {s}\n", .{path});
- std.process.exit(1);
- };
-
- const colon = std.mem.indexOfScalar(u8, args[3], ':') orelse usage();
- const line = std.fmt.parseInt(u32, args[3][0..colon], 10) catch usage();
- const col = std.fmt.parseInt(u32, args[3][colon + 1 ..], 10) catch usage();
- if (line == 0 or col == 0) usage();
-
- var arg: []const u8 = "";
- var reps: u32 = 1;
- var warm_s: u32 = 0;
- var i: usize = 4;
- while (i < args.len) : (i += 1) {
- if (std.mem.eql(u8, args[i], "--reps")) {
- i += 1;
- if (i >= args.len) usage();
- reps = @max(1, std.fmt.parseInt(u32, args[i], 10) catch usage());
- } else if (std.mem.eql(u8, args[i], "--warm")) {
- i += 1;
- if (i >= args.len) usage();
- warm_s = std.fmt.parseInt(u32, args[i], 10) catch usage();
- } else arg = args[i];
- }
-
- lsp.setStatusSink(null, statusSink);
-
- // 1-based line:col -> byte offset
- var off: usize = 0;
- var ln: u32 = 1;
- while (ln < line and off < source.len) : (off += 1) {
- if (source[off] == '\n') ln += 1;
- }
- off = @min(off + col - 1, source.len);
-
- const root = std.fs.path.dirname(path) orelse "/";
- const timings = try arena.alloc(u64, reps);
- var stdout_buf: [64 * 1024]u8 = undefined;
- var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
- var first_query = true;
-
- for (kinds[0..nkinds]) |kind| {
- var rows: []const u8 = "";
- for (0..reps) |r| {
- // one warm pause for the whole battery, right after the very
- // first (cold, server-spawning) query — the status lines on
- // stderr narrate what the server does with the time
- var out: std.Io.Writer.Allocating = .init(gpa);
- defer out.deinit();
- var scratch: std.heap.ArenaAllocator = .init(gpa);
- defer scratch.deinit();
- const t0 = nowUs();
- try lsp.query(gpa, scratch.allocator(), .{
- .kind = kind,
- .path = path,
- .source = source,
- .offset = @intCast(off),
- .arg = arg,
- .root = root,
- }, &out.writer);
- timings[r] = nowUs() -| t0;
- rows = try arena.dupe(u8, out.written());
- if (first_query) {
- first_query = false;
- if (warm_s > 0) for (0..warm_s * 10) |_| {
- _ = usleep(100_000);
- };
- }
- }
-
- try stdout.interface.print("## {t}\n", .{kind});
- try stdout.interface.writeAll(rows);
- try stdout.interface.flush();
-
- const n = std.mem.count(u8, rows, "\n");
- const cold = timings[0];
- std.mem.sort(u64, timings[0..reps], {}, std.sort.asc(u64));
- if (reps == 1)
- std.debug.print("# {t}: {d} row(s) in {d}us\n", .{ kind, n, cold })
- else
- std.debug.print("# {t}: {d} row(s); first {d}us, min {d}us, median {d}us ({d} reps)\n", .{
- kind, n, cold, timings[0], timings[reps / 2], reps,
- });
- }
-}