summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/lspprobe.zig184
1 files changed, 184 insertions, 0 deletions
diff --git a/tools/lspprobe.zig b/tools/lspprobe.zig
new file mode 100644
index 00000000..8b953cec
--- /dev/null
+++ b/tools/lspprobe.zig
@@ -0,0 +1,184 @@
+//! 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);
+ var scratch: std.heap.ArenaAllocator = .init(gpa);
+ defer scratch.deinit();
+ const t0 = nowUs();
+ 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());
+ out.deinit();
+ 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,
+ });
+ }
+}