mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-10 08:08:16 -05:00
feat: take Bun profile (#8515)
**Motivation** - to be able to take profile in Bun **Description** - implement `profileBun` api using `console.profile()` apis - as tested, it only supported up to 3s or Bun will crash so I have to do a for loop - cannot take the whole epoch, or `debug.bun.sh` will take forever to load - refactor: implement `profileThread` as wrapper of either `profileNodeJS` or `profileBun` - note that NodeJS and Bun works a bit differently: - NodeJS: we can persist to a file, log into server and copy it - Bun: need to launch `debug.bun.sh` web page as the inspector, profile will be flushed from node to to the inspected and rendered live there **Steps to take profile** - start beacon node with `--inspect` and look for `debug.bun.sh` log - launch the specified url, for example `https://debug.bun.sh/#127.0.0.1:9229/0qoflywrwso` - (optional) the UI does not show if the inspector is connected to app or not, so normally I wait for the sources to be launched - `curl -X POST http://localhost:9596/eth/v1/lodestar/write_profile?thread=main` - look into `Timelines` tab in `https://debug.bun.sh/`, check `Call tree` there - (optional) export Timeline to share it **Sample Profile** [Timeline%20Recording%201 (4).json.zip](https://github.com/user-attachments/files/22788370/Timeline.20Recording.201.4.json.zip) --------- Co-authored-by: Tuyen Nguyen <twoeths@users.noreply.github.com>
This commit is contained in:
@@ -117,7 +117,7 @@ export type Endpoints = {
|
||||
dirpath?: string;
|
||||
},
|
||||
{query: {thread?: LodestarThreadType; duration?: number; dirpath?: string}},
|
||||
{filepath: string},
|
||||
{result: string},
|
||||
EmptyMeta
|
||||
>;
|
||||
/** TODO: description */
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {Tree} from "@chainsafe/persistent-merkle-tree";
|
||||
import {routes} from "@lodestar/api";
|
||||
import {ApplicationMethods} from "@lodestar/api/server";
|
||||
@@ -13,7 +11,7 @@ import {BeaconChain} from "../../../chain/index.js";
|
||||
import {QueuedStateRegenerator, RegenRequest} from "../../../chain/regen/index.js";
|
||||
import {IBeaconDb} from "../../../db/interface.js";
|
||||
import {GossipType} from "../../../network/index.js";
|
||||
import {profileNodeJS, writeHeapSnapshot} from "../../../util/profile.js";
|
||||
import {ProfileThread, profileThread, writeHeapSnapshot} from "../../../util/profile.js";
|
||||
import {getStateResponseWithRegen} from "../beacon/state/utils.js";
|
||||
import {ApiModules} from "../types.js";
|
||||
|
||||
@@ -26,7 +24,9 @@ export function getLodestarApi({
|
||||
}: Pick<ApiModules, "chain" | "config" | "db" | "network" | "sync">): ApplicationMethods<routes.lodestar.Endpoints> {
|
||||
let writingHeapdump = false;
|
||||
let writingProfile = false;
|
||||
const defaultProfileMs = SLOTS_PER_EPOCH * config.SLOT_DURATION_MS;
|
||||
// for NodeJS, profile the whole epoch
|
||||
// for Bun, profile 1 slot. Otherwise it will either crash the app, and/or inspector cannot render the profile
|
||||
const defaultProfileMs = globalThis.Bun ? config.SLOT_DURATION_MS : SLOTS_PER_EPOCH * config.SLOT_DURATION_MS;
|
||||
|
||||
return {
|
||||
async writeHeapdump({thread = "main", dirpath = "."}) {
|
||||
@@ -63,7 +63,6 @@ export function getLodestarApi({
|
||||
|
||||
try {
|
||||
let filepath: string;
|
||||
let profile: string;
|
||||
switch (thread) {
|
||||
case "network":
|
||||
filepath = await network.writeNetworkThreadProfile(duration, dirpath);
|
||||
@@ -73,12 +72,10 @@ export function getLodestarApi({
|
||||
break;
|
||||
default:
|
||||
// main thread
|
||||
profile = await profileNodeJS(duration);
|
||||
filepath = path.join(dirpath, `main_thread_${new Date().toISOString()}.cpuprofile`);
|
||||
fs.writeFileSync(filepath, profile);
|
||||
filepath = await profileThread(ProfileThread.MAIN, duration, dirpath);
|
||||
break;
|
||||
}
|
||||
return {data: {filepath}};
|
||||
return {data: {result: filepath}};
|
||||
} finally {
|
||||
writingProfile = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import worker from "node:worker_threads";
|
||||
import {privateKeyFromProtobuf} from "@libp2p/crypto/keys";
|
||||
import {peerIdFromPrivateKey} from "@libp2p/peer-id";
|
||||
@@ -11,7 +9,7 @@ import {RegistryMetricCreator, collectNodeJSMetrics} from "../../metrics/index.j
|
||||
import {AsyncIterableBridgeCaller, AsyncIterableBridgeHandler} from "../../util/asyncIterableToEvents.js";
|
||||
import {Clock} from "../../util/clock.js";
|
||||
import {peerIdToString} from "../../util/peerId.js";
|
||||
import {profileNodeJS, writeHeapSnapshot} from "../../util/profile.js";
|
||||
import {ProfileThread, profileThread, writeHeapSnapshot} from "../../util/profile.js";
|
||||
import {wireEventsOnWorkerThread} from "../../util/workerEvents.js";
|
||||
import {NetworkEventBus, NetworkEventData, networkEventDirection} from "../events.js";
|
||||
import {
|
||||
@@ -157,10 +155,7 @@ const libp2pWorkerApi: NetworkWorkerApi = {
|
||||
dumpDiscv5KadValues: () => core.dumpDiscv5KadValues(),
|
||||
dumpMeshPeers: () => core.dumpMeshPeers(),
|
||||
writeProfile: async (durationMs: number, dirpath: string) => {
|
||||
const profile = await profileNodeJS(durationMs);
|
||||
const filePath = path.join(dirpath, `network_thread_${new Date().toISOString()}.cpuprofile`);
|
||||
fs.writeFileSync(filePath, profile);
|
||||
return filePath;
|
||||
return profileThread(ProfileThread.NETWORK, durationMs, dirpath);
|
||||
},
|
||||
writeDiscv5Profile: async (durationMs: number, dirpath: string) => {
|
||||
return core.writeDiscv5Profile(durationMs, dirpath);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import worker from "node:worker_threads";
|
||||
import {privateKeyFromProtobuf} from "@libp2p/crypto/keys";
|
||||
import {peerIdFromPrivateKey} from "@libp2p/peer-id";
|
||||
@@ -14,7 +12,7 @@ import {Gauge} from "@lodestar/utils";
|
||||
import {RegistryMetricCreator} from "../../metrics/index.js";
|
||||
import {collectNodeJSMetrics} from "../../metrics/nodeJsMetrics.js";
|
||||
import {Clock} from "../../util/clock.js";
|
||||
import {profileNodeJS, writeHeapSnapshot} from "../../util/profile.js";
|
||||
import {ProfileThread, profileThread, writeHeapSnapshot} from "../../util/profile.js";
|
||||
import {Discv5WorkerApi, Discv5WorkerData} from "./types.js";
|
||||
import {ENRRelevance, enrRelevance} from "./utils.js";
|
||||
|
||||
@@ -108,10 +106,7 @@ const module: Discv5WorkerApi = {
|
||||
return (await metricsRegistry?.metrics()) ?? "";
|
||||
},
|
||||
writeProfile: async (durationMs: number, dirpath: string) => {
|
||||
const profile = await profileNodeJS(durationMs);
|
||||
const filePath = path.join(dirpath, `discv5_thread_${new Date().toISOString()}.cpuprofile`);
|
||||
fs.writeFileSync(filePath, profile);
|
||||
return filePath;
|
||||
return profileThread(ProfileThread.DISC5, durationMs, dirpath);
|
||||
},
|
||||
writeHeapSnapshot: async (prefix: string, dirpath: string) => {
|
||||
return writeHeapSnapshot(prefix, dirpath);
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {sleep} from "@lodestar/utils";
|
||||
|
||||
export enum ProfileThread {
|
||||
MAIN = "main",
|
||||
NETWORK = "network",
|
||||
DISC5 = "discv5",
|
||||
}
|
||||
|
||||
/**
|
||||
* Take 10m profile of the current thread without promise tracking.
|
||||
* The time to take a Bun profile.
|
||||
* If we increase this time it'll potentiall cause the app to crash.
|
||||
* If we decrease this time, profile recorded will be fragmented and hard to analyze.
|
||||
*/
|
||||
export async function profileNodeJS(durationMs: number): Promise<string> {
|
||||
const BUN_PROFILE_MS = 3 * 1000;
|
||||
|
||||
export async function profileThread(thread: ProfileThread, durationMs: number, dirpath: string): Promise<string> {
|
||||
return globalThis.Bun ? profileBun(thread, durationMs) : profileNodeJS(thread, durationMs, dirpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take `durationMs` profile of the current thread and return the persisted file path.
|
||||
*/
|
||||
async function profileNodeJS(thread: ProfileThread, durationMs: number, dirpath: string): Promise<string> {
|
||||
const inspector = await import("node:inspector");
|
||||
|
||||
// due to some typing issues, not able to use promisify here
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const profile = await new Promise<string>((resolve, reject) => {
|
||||
// Start the inspector and connect to it
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
@@ -29,6 +48,29 @@ export async function profileNodeJS(durationMs: number): Promise<string> {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const filePath = path.join(dirpath, `${thread}_thread_${new Date().toISOString()}.cpuprofile`);
|
||||
fs.writeFileSync(filePath, profile);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike NodeJS, Bun console.profile() api flush data to the inspector,
|
||||
* so this api returns ms taken of this profile instead of file path.
|
||||
*/
|
||||
async function profileBun(thread: ProfileThread, durationMs: number): Promise<string> {
|
||||
const start = Date.now();
|
||||
let now = Date.now();
|
||||
while (now - start < durationMs) {
|
||||
// biome-ignore lint/suspicious/noConsole: need to use console api to profile in Bun
|
||||
console.profile(String(now));
|
||||
await sleep(BUN_PROFILE_MS);
|
||||
// biome-ignore lint/suspicious/noConsole: need to use console api to profile in Bun
|
||||
console.profileEnd(String(now));
|
||||
now = Date.now();
|
||||
}
|
||||
|
||||
return `Successfully take Bun ${thread} thread profile in ${now - start}ms. Check your inspector to see the profile.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user