tiny.profiling.report.serve
Defined in report.
API (6)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: src/profiling/report/root.zig:7
zig
pub const serve = @import("serve.zig");Source: src/profiling/report/serve.zig
zig
const std = @import("std");const alloc = @import("alloc");const http = @import("http");const pretty_usage = @import("pretty_usage");const sys = @import("sys");const site = @import("root.zig").site;const max_page_bytes = 512 * 1024 * 1024;pub const Options = struct { site: site.Options = .{}, address: []const u8 = "127.0.0.1", port: u16 = 8787,};pub const Generated = struct { runs: usize, pages: usize,};pub const State = struct { allocator: std.mem.Allocator, options: Options, output_dir: []const u8 = "", newest_seen: i128 = 0, mutex: std.atomic.Mutex = .unlocked, pub fn init(allocator: std.mem.Allocator, options: Options) State { return .{ .allocator = allocator, .options = options }; } pub fn regenerate(self: *State) !Generated { var scratch_state = std.heap.ArenaAllocator.init(self.allocator); defer scratch_state.deinit(); const result = try site.generate(scratch_state.allocator(), self.options.site); const output_dir = try self.allocator.dupe(u8, result.output_dir); if (self.output_dir.len != 0) self.allocator.free(self.output_dir); self.output_dir = output_dir; self.newest_seen = newestArtifact(self.allocator, self.options.site.profiling_dir) orelse 0; return .{ .runs = result.runs, .pages = result.pages }; } fn refresh(self: *State) void { const newest = newestArtifact(self.allocator, self.options.site.profiling_dir) orelse return; if (newest <= self.newest_seen) return; const result = self.regenerate() catch |err| { pretty_usage.Terminal.stderr(self.allocator, .{}).writeTextFmt( "profile web: regeneration failed, serving the previous site: {s}\n", .{@errorName(err)}, ) catch {}; return; }; pretty_usage.Terminal.stderr(self.allocator, .{}).writeTextFmt( "profile web: regenerated {d} page(s) for {d} run(s)\n", .{ result.pages, result.runs }, ) catch {}; } fn lock(self: *State) void { while (!self.mutex.tryLock()) sys.thread.yield(); } fn unlock(self: *State) void { self.mutex.unlock(); }};fn newestArtifact(allocator: std.mem.Allocator, profiling_dir: []const u8) ?i128 { var scratch_state = std.heap.ArenaAllocator.init(allocator); defer scratch_state.deinit(); const scratch = scratch_state.allocator(); const dir_stat = sys.fs.statFile(profiling_dir) catch return null; var newest: i128 = dir_stat.mtime.nanoseconds; const listing = sys.fs.listDirAlloc(scratch, profiling_dir) catch return newest; for (listing) |item| { if (item.kind != .directory) continue; const manifest = std.fs.path.join(scratch, &.{ item.path, "manifest.json" }) catch continue; const stat = sys.fs.statFile(manifest) catch continue; newest = @max(newest, @as(i128, stat.mtime.nanoseconds)); } return newest;}pub fn run(options: Options) !void { const allocator = alloc.workerAllocator(); var state = State.init(allocator, options); const first = try state.regenerate(); var router = http.Router(*State).init(allocator, &state); defer router.deinit(); router.notFound(serveFile); const server = try http.Server.init(allocator, .{ .address = options.address, .port = options.port, }); defer server.deinit(); const port = try sys.net.socketPort(server.listener.?); server.setHandler(&router, handleConnection); try pretty_usage.Terminal.stdout(allocator, .{}).writeTextFmt( "profile web: {d} run(s), {d} page(s) in {s}\nserving http://{s}:{d}/ (Ctrl-C to stop)\n", .{ first.runs, first.pages, state.output_dir, options.address, port }, ); try server.listen();}fn handleConnection(router: *http.Router(*State), conn: *http.Connection) void { while (router.handleRequest(conn) catch false) {}}fn releaseFileBody(state: *State, body: []const u8) void { state.allocator.free(body);}fn serveFile(state: *State, req: *http.Request, res: *http.Response) !void { state.lock(); defer state.unlock(); const target = req.pathOnly(); if (std.mem.eql(u8, target, "/") or std.mem.eql(u8, target, "/index.html")) state.refresh(); const relative = resolveRelative(state.allocator, target) catch { res.status = 400; res.status_text = "Bad Request"; res.body = "Bad Request"; return; }; defer state.allocator.free(relative); const full = try std.fs.path.join(state.allocator, &.{ state.output_dir, relative }); defer state.allocator.free(full); const body = sys.fs.readFileAlloc(state.allocator, full, max_page_bytes) catch { res.status = 404; res.status_text = "Not Found"; res.body = "Not Found"; try res.setHeader("Content-Type", "text/plain"); return; }; res.setBodyWithRelease(body, state, releaseFileBody); try res.setHeader("Content-Type", contentType(relative)); try res.setHeader("Cache-Control", "no-store");}fn resolveRelative(allocator: std.mem.Allocator, target: []const u8) ![]const u8 { var trimmed = std.mem.trimStart(u8, target, "/"); if (trimmed.len == 0) trimmed = "index.html"; var parts = std.mem.splitScalar(u8, trimmed, '/'); while (parts.next()) |part| { if (std.mem.eql(u8, part, "..") or std.mem.eql(u8, part, ".")) return error.PathEscapes; } if (trimmed[trimmed.len - 1] == '/') { return try std.fmt.allocPrint(allocator, "{s}index.html", .{trimmed}); } return try allocator.dupe(u8, trimmed);}fn contentType(path: []const u8) []const u8 { const extension = std.fs.path.extension(path); if (std.mem.eql(u8, extension, ".html")) return "text/html; charset=utf-8"; if (std.mem.eql(u8, extension, ".css")) return "text/css"; if (std.mem.eql(u8, extension, ".js")) return "text/javascript"; if (std.mem.eql(u8, extension, ".svg")) return "image/svg+xml"; if (std.mem.eql(u8, extension, ".json")) return "application/json"; if (std.mem.eql(u8, extension, ".txt")) return "text/plain; charset=utf-8"; if (std.mem.eql(u8, extension, ".png")) return "image/png"; return "application/octet-stream";}test "serve resolves paths and rejects escapes" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); try std.testing.expectEqualStrings("index.html", try resolveRelative(allocator, "/")); try std.testing.expectEqualStrings("index.html", try resolveRelative(allocator, "")); try std.testing.expectEqualStrings("runs/a/index.html", try resolveRelative(allocator, "/runs/a/")); try std.testing.expectEqualStrings("runs/a/workloads/w.html", try resolveRelative(allocator, "/runs/a/workloads/w.html")); try std.testing.expectError(error.PathEscapes, resolveRelative(allocator, "/../etc/passwd")); try std.testing.expectError(error.PathEscapes, resolveRelative(allocator, "/runs/../../x"));}test "serve names content types by extension" { try std.testing.expectEqualStrings("text/html; charset=utf-8", contentType("index.html")); try std.testing.expectEqualStrings("text/css", contentType("profile.css")); try std.testing.expectEqualStrings("application/json", contentType("analysis.json")); try std.testing.expectEqualStrings("application/octet-stream", contentType("perf.data"));}fn test_listen(server: *http.Server) void { server.listen() catch {};}test "serve regenerates and serves the site over http" { const model = @import("root.zig").model; var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const profiling_dir = ".zig-cache/profile-web-serve-test"; const output_dir = ".zig-cache/profile-web-serve-test-out"; defer sys.fs.deleteTree(profiling_dir) catch {}; defer sys.fs.deleteTree(output_dir) catch {}; try model.writeTestFixture(allocator, profiling_dir); var state = State.init(allocator, .{ .site = .{ .profiling_dir = profiling_dir, .output_dir = output_dir }, }); const generated = try state.regenerate(); try std.testing.expectEqual(@as(usize, 9), generated.pages); var router = http.Router(*State).init(allocator, &state); defer router.deinit(); router.notFound(serveFile); const server = try http.Server.init(std.testing.allocator, .{ .port = 0, .num_workers = 1 }); defer { server.stop(); server.deinit(); } const port = try sys.net.socketPort(server.listener.?); server.setHandler(&router, handleConnection); const listen_thread = try sys.thread.spawn(test_listen, .{server}); defer { server.stop(); listen_thread.join(); } sys.time.sleepMilliseconds(10); var client = http.Client.init(std.testing.allocator); defer client.deinit(); var client_operation_storage = try http.ClientOperationStorage.init(std.testing.allocator, .{ .operation_count = 1, .host_bytes_per_operation = http.default_client_operation_host_bytes, .target_bytes_per_operation = http.default_client_operation_target_bytes, .plain_read_bytes_per_operation = http.default_client_operation_plain_read_bytes, .plain_write_bytes_per_operation = http.default_client_operation_plain_write_bytes, }); defer client_operation_storage.deinit(std.testing.allocator); client_operation_storage.activate(); const operation_scratch = try client_operation_storage.operation(0); var client_response_storage = try http.ClientResponseStorage.init(std.testing.allocator, .{ .response_count = 1, .header_count_per_response = 16, .head_bytes_per_response = 4096, .body_bytes_per_response = 1024 * 1024, }); defer client_response_storage.deinit(std.testing.allocator); client_response_storage.activate(); const response_scratch = try client_response_storage.response(0); var url_buffer: [128]u8 = undefined; const index_response = try client.get( operation_scratch, response_scratch, try std.fmt.bufPrint(&url_buffer, "http://127.0.0.1:{d}/", .{port}), &.{}, ); try std.testing.expectEqual(@as(u16, 200), index_response.status); try std.testing.expect(std.mem.indexOf(u8, index_response.body, "tiny profiling") != null); const flame_response = try client.get( operation_scratch, response_scratch, try std.fmt.bufPrint( &url_buffer, "http://127.0.0.1:{d}/runs/run-200-bbb/workloads/gpalloc.allocator.flame.html", .{port}, ), &.{}, ); try std.testing.expectEqual(@as(u16, 200), flame_response.status); try std.testing.expect(std.mem.indexOf(u8, flame_response.body, "flame-cpu") != null); const missing_response = try client.get( operation_scratch, response_scratch, try std.fmt.bufPrint(&url_buffer, "http://127.0.0.1:{d}/nope.html", .{port}), &.{}, ); try std.testing.expectEqual(@as(u16, 404), missing_response.status); const escape_response = try client.get( operation_scratch, response_scratch, try std.fmt.bufPrint(&url_buffer, "http://127.0.0.1:{d}/../build.zig", .{port}), &.{}, ); try std.testing.expect(escape_response.status == 400 or escape_response.status == 404);}Audit
| Definitions | 7 |
|---|---|
| Public names | 7 |
| Members | 10 |
| Version | 26.7.0 |
| Revision | daab053ee433 |