Files
bun.sh/src/bake/DevServer/memory_cost.zig
Zack Radisic 71e2161591 Split DevServer.zig into multiple files (#21299)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-07-23 05:29:22 +00:00

224 lines
8.7 KiB
Zig

pub const MemoryCost = @This();
incremental_graph_client: usize,
incremental_graph_server: usize,
js_code: usize,
source_maps: usize,
assets: usize,
other: usize,
/// Returns an estimation for how many bytes DevServer is explicitly aware of.
/// If this number stays constant but RSS grows, then there is a memory leak. If
/// this number grows out of control, then incremental garbage collection is not
/// good enough.
///
/// Memory measurements are important as DevServer has a long lifetime, but
/// unlike the HTTP server, it controls a lot of objects that are frequently
/// being added, removed, and changed (as the developer edits source files). It
/// is exponentially easy to mess up memory management.
pub fn memoryCostDetailed(dev: *DevServer) MemoryCost {
var other_bytes: usize = @sizeOf(DevServer);
var incremental_graph_client: usize = 0;
var incremental_graph_server: usize = 0;
var js_code: usize = 0;
var source_maps: usize = 0;
var assets: usize = 0;
const dedupe_bits: u32 = @truncate(@abs(std.time.nanoTimestamp()));
const discard = voidFieldTypeDiscardHelper;
// See https://github.com/ziglang/zig/issues/21879
_ = VoidFieldTypes(DevServer){
// does not contain pointers
.allocator = {},
.assume_perfect_incremental_bundling = {},
.bun_watcher = {},
.bundles_since_last_error = {},
.configuration_hash_key = {},
.inspector_server_id = {},
.deferred_request_pool = {},
.dump_dir = {},
.emit_incremental_visualizer_events = {},
.emit_memory_visualizer_events = {},
.frontend_only = {},
.generation = {},
.graph_safety_lock = {},
.has_pre_crash_handler = {},
.magic = {},
.memory_visualizer_timer = {},
.plugin_state = {},
.relative_path_buf_lock = {},
.server_register_update_callback = {},
.server_fetch_function_callback = {},
.watcher_atomics = {},
.relative_path_buf = {},
// pointers that are not considered a part of DevServer
.vm = {},
.server = {},
.server_transpiler = {},
.client_transpiler = {},
.ssr_transpiler = {},
.log = {},
.framework = {},
.bundler_options = {},
.allocation_scope = {},
.broadcast_console_log_from_browser_to_server = {},
// to be counted.
.root = {
other_bytes += dev.root.len;
},
.router = {
other_bytes += dev.router.memoryCost();
},
.route_bundles = for (dev.route_bundles.items) |*bundle| {
other_bytes += bundle.memoryCost();
},
.server_graph = {
const cost = dev.server_graph.memoryCostDetailed(dedupe_bits);
incremental_graph_server += cost.graph;
js_code += cost.code;
source_maps += cost.source_maps;
},
.client_graph = {
const cost = dev.client_graph.memoryCostDetailed(dedupe_bits);
incremental_graph_client += cost.graph;
js_code += cost.code;
source_maps += cost.source_maps;
},
.assets = {
assets += dev.assets.memoryCost();
},
.active_websocket_connections = {
other_bytes += dev.active_websocket_connections.capacity() * @sizeOf(*HmrSocket);
},
.source_maps = {
other_bytes += memoryCostArrayHashMap(dev.source_maps.entries);
for (dev.source_maps.entries.values()) |entry| {
source_maps += entry.files.memoryCost();
for (entry.files.items(.tags), entry.files.items(.data)) |tag, data| {
switch (tag) {
.ref => source_maps += data.ref.data.memoryCostWithDedupe(dedupe_bits),
.empty => {},
}
}
}
},
.incremental_result = discard(VoidFieldTypes(IncrementalResult){
.had_adjusted_edges = {},
.client_components_added = {
other_bytes += memoryCostArrayList(dev.incremental_result.client_components_added);
},
.framework_routes_affected = {
other_bytes += memoryCostArrayList(dev.incremental_result.framework_routes_affected);
},
.client_components_removed = {
other_bytes += memoryCostArrayList(dev.incremental_result.client_components_removed);
},
.failures_removed = {
other_bytes += memoryCostArrayList(dev.incremental_result.failures_removed);
},
.client_components_affected = {
other_bytes += memoryCostArrayList(dev.incremental_result.client_components_affected);
},
.failures_added = {
other_bytes += memoryCostArrayList(dev.incremental_result.failures_added);
},
.html_routes_soft_affected = {
other_bytes += memoryCostArrayList(dev.incremental_result.html_routes_soft_affected);
},
.html_routes_hard_affected = {
other_bytes += memoryCostArrayList(dev.incremental_result.html_routes_hard_affected);
},
}),
.has_tailwind_plugin_hack = if (dev.has_tailwind_plugin_hack) |hack| {
other_bytes += memoryCostArrayHashMap(hack);
},
.directory_watchers = {
other_bytes += memoryCostArrayList(dev.directory_watchers.dependencies);
other_bytes += memoryCostArrayList(dev.directory_watchers.dependencies_free_list);
other_bytes += memoryCostArrayHashMap(dev.directory_watchers.watches);
for (dev.directory_watchers.dependencies.items) |dep| {
other_bytes += dep.specifier.len;
}
for (dev.directory_watchers.watches.keys()) |dir_name| {
other_bytes += dir_name.len;
}
},
.html_router = {
// std does not provide a way to measure exact allocation size of HashMapUnmanaged
other_bytes += dev.html_router.map.capacity() * (@sizeOf(*HTMLBundle.HTMLBundleRoute) + @sizeOf([]const u8));
// DevServer does not count the referenced HTMLBundle.HTMLBundleRoutes
},
.bundling_failures = {
other_bytes += memoryCostSlice(dev.bundling_failures.keys());
for (dev.bundling_failures.keys()) |failure| {
other_bytes += failure.data.len;
}
},
// All entries are owned by the bundler arena, not DevServer, except for `requests`
.current_bundle = if (dev.current_bundle) |bundle| {
var r = bundle.requests.first;
while (r) |request| : (r = request.next) {
other_bytes += @sizeOf(DeferredRequest.Node);
}
},
.next_bundle = {
var r = dev.next_bundle.requests.first;
while (r) |request| : (r = request.next) {
other_bytes += @sizeOf(DeferredRequest.Node);
}
other_bytes += memoryCostArrayHashMap(dev.next_bundle.route_queue);
},
.route_lookup = {
other_bytes += memoryCostArrayHashMap(dev.route_lookup);
},
.testing_batch_events = switch (dev.testing_batch_events) {
.disabled => {},
.enabled => |batch| {
other_bytes += memoryCostArrayHashMap(batch.entry_points.set);
},
.enable_after_bundle => {},
},
};
return .{
.assets = assets,
.incremental_graph_client = incremental_graph_client,
.incremental_graph_server = incremental_graph_server,
.js_code = js_code,
.other = other_bytes,
.source_maps = source_maps,
};
}
pub fn memoryCost(dev: *DevServer) usize {
const cost = memoryCostDetailed(dev);
var acc: usize = 0;
inline for (@typeInfo(MemoryCost).@"struct".fields) |field| {
acc += @field(cost, field.name);
}
return acc;
}
pub fn memoryCostArrayList(slice: anytype) usize {
return slice.capacity * @sizeOf(@typeInfo(@TypeOf(slice.items)).pointer.child);
}
pub fn memoryCostSlice(slice: anytype) usize {
return slice.len * @sizeOf(@typeInfo(@TypeOf(slice)).pointer.child);
}
pub fn memoryCostArrayHashMap(map: anytype) usize {
return @TypeOf(map.entries).capacityInBytes(map.entries.capacity);
}
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;
const HTMLBundle = jsc.API.HTMLBundle;
const DevServer = bun.bake.DevServer;
const DeferredRequest = DevServer.DeferredRequest;
const HmrSocket = DevServer.HmrSocket;
const IncrementalResult = DevServer.IncrementalResult;
const VoidFieldTypes = bun.meta.VoidFieldTypes;
const voidFieldTypeDiscardHelper = bun.meta.voidFieldTypeDiscardHelper;