//! One language query against the REAL seam, from the command line. //! //! pardes-lspprobe : [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 : [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, }); } }