Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions simple-express-app/csv-visualizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2026 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

const fs = require('node:fs/promises')
const path = require('path')
const express = require('express')
const app = express()
const PORT = 3001
const monitorDir = path.join(__dirname, 'monitor-data')

app.use(express.static(path.join(__dirname, 'public')))

// List all CSV files in the directory
app.get('/csv-list', async (req, res) => {
const contents = await fs.readdir(monitorDir)
const files = contents.filter((file) => file.endsWith('.csv'))
res.json(files)
})
Comment thread Dismissed

// Fetch CSV data for a given file
app.get('/data', async (req, res) => {
const file = req.query.file
if (!file || !file.endsWith('.csv')) {
res.status(400).send('Invalid CSV file')
return
}

let csvPath
try {
csvPath = await fs.realpath(path.resolve(monitorDir, file))
Comment thread Dismissed
} catch (err) {
res.status(404).send(`Failed to find file ${err.message}`)
return
}

const csv = await fs.readFile(csvPath, 'utf8')
Comment thread Fixed
const lines = csv.trim().split('\n')
const headers = lines.shift().split(',')
// formats the data into a collection:
// [
// { timestamp, cpu_user_ms, cpu_system_ms, etc }
// ]
const data = lines.map((line) => {
const values = line.split(',')
const obj = {}
headers.forEach((h, i) => {
obj[h] = values[i]
})
return obj
})
res.json({ headers, data })
})
Comment thread Dismissed

app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`CSV visualizer running at http://localhost:${PORT}`)
})
5 changes: 5 additions & 0 deletions simple-express-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ app.get('/sns', async (req, res) => {
})

app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`))

process.on('SIGINT', () => {
console.log('ending app server')
process.exit(0)
})
7 changes: 5 additions & 2 deletions simple-express-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node -r newrelic --env-file .env index.js"
"start": "node -r newrelic --env-file .env index.js",
"start:perf:no-agent": "node -r ./v8-monitor.js index.js",
"start:perf:agent-no-profiling": "node -r ./v8-monitor.js -r newrelic --env-file .env_no_prof index.js",
"start:perf:agent-profiling": "node -r ./v8-monitor.js -r newrelic --env-file .env index.js"
},
"author": "",
"license": "ISC",
Expand All @@ -14,6 +17,6 @@
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"express": "^4.18.2",
"newrelic": "^12.5.0"
"newrelic": "^13.15.0"
}
}
101 changes: 101 additions & 0 deletions simple-express-app/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!DOCTYPE html>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you claude, this is good enough for a "front-end"

<html lang="en">
<head>
<meta charset="UTF-8">
<title>V8 Monitor CSV Visualization</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: sans-serif; margin: 2em; }
#controls { margin-bottom: 1em; }
canvas { max-width: 100%; }
</style>
</head>
<body>
<h1>V8 Monitor CSV Visualization</h1>
<div id="controls">
<label for="csvSelect">CSV File:</label>
<select id="csvSelect"></select>
</div>
<div id="charts"></div>
<script>
async function fetchCsvList() {
const res = await fetch('/csv-list');
return await res.json();
}

async function fetchData(file) {
const res = await fetch(`/data?file=${encodeURIComponent(file)}`);
return await res.json();
}

function renderAllCharts(headers, data) {
const chartsDiv = document.getElementById('charts');
chartsDiv.innerHTML = '';
const timestamps = data.map(row => row.timestamp);
headers.forEach(metric => {
if (metric === 'timestamp') return;
const values = data.map(row => parseFloat(row[metric]));
const container = document.createElement('div');
container.style.marginBottom = '2em';
const label = document.createElement('h3');
label.textContent = metric;
container.appendChild(label);
const canvas = document.createElement('canvas');
canvas.height = 80;
container.appendChild(canvas);
chartsDiv.appendChild(container);
const ctx = canvas.getContext('2d');
// Use distinct colors for percent metrics
let color = 'blue';
let yLabel = metric;
if (metric.endsWith('Percent')) {
if (metric === 'heapUsedPercent') color = 'green';
if (metric === 'cpuUserPercent') color = 'orange';
if (metric === 'cpuSystemPercent') color = 'red';
yLabel = metric + ' (%)';
}
new Chart(ctx, {
type: 'line',
data: {
labels: timestamps,
datasets: [{
label: metric,
data: values,
borderColor: color,
fill: false,
}],
},
options: {
scales: {
x: { display: true, title: { display: true, text: 'Timestamp' } },
y: { display: true, title: { display: true, text: yLabel } },
},
},
});
});
}

async function main() {
const csvList = await fetchCsvList();
const csvSelect = document.getElementById('csvSelect');
csvList.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file;
csvSelect.appendChild(option);
});
async function loadAndRender(file) {
const { headers, data } = await fetchData(file);
renderAllCharts(headers, data);
}
csvSelect.addEventListener('change', () => {
loadAndRender(csvSelect.value);
});
if (csvList.length > 0) {
loadAndRender(csvList[0]);
}
}
main();
</script>
</body>
</html>
35 changes: 35 additions & 0 deletions simple-express-app/run-profiling-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
RUN_TIME=1
commands=("start:perf:no-agent" "start:perf:agent-no-profiling" "start:perf:agent-profiling")

echo "Removing leftover monitoring files from past runs"
rm monitor-data/*

for cmd in "${commands[@]}"; do
# Start the app in the background
echo "Starting app with $cmd"
npm run "$cmd" &
APP_PID=$!

# Wait for the server to start (adjust as needed)
sleep 10

echo "Injecting traffic for $RUN_TIME minutes"
hey -z "$RUN_TIME"m http://localhost:3000/named-route

# After hey completes, stop the app
kill $APP_PID
# Determine output filename
case "$cmd" in
"start:perf:no-agent") out="perf-data-no-agent.csv" ;;
"start:perf:agent-no-profiling") out="perf-data-agent-no-profiling.csv" ;;
"start:perf:agent-profiling") out="perf-data-agent-profiling.csv" ;;
esac

echo "moving file to $out"
# Find the most recent CSV file in monitor-data and rename it
# Using /bin/ls because my shell alias ls to some other cli tool
CSV_FILE=$(/bin/ls -t monitor-data/*.csv | head -n 1)
mv "$CSV_FILE" monitor-data/"$out"
echo "done, onto the next"
done
115 changes: 115 additions & 0 deletions simple-express-app/v8-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2026 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

const fs = require('node:fs/promises')
const path = require('node:path')

const SAMPLE_INTERVAL = 1000 // ms
const WARMUP_DURATION = 5000 // ms (default warmup)
const OUTPUT_FILE = `v8-monitor-${Date.now()}.csv`
const MONITORING_DIR = path.join(__dirname, 'monitor-data')
const FILE_PATH = path.join(MONITORING_DIR, OUTPUT_FILE)

function getCpuUsage(prevCpu) {
// Get CPU usage delta from process
const usage = process.cpuUsage(prevCpu)
const formatted = {
user: usage.user / 1000,
system: usage.system / 1000,
}
return {
raw: usage,
formatted,
}
}

function getMemoryUsage() {
const mem = process.memoryUsage()
return {
rss: mem.rss,
heapTotal: mem.heapTotal,
heapUsed: mem.heapUsed,
}
}

async function writeHeader() {
try {
await fs.mkdir(MONITORING_DIR)
} catch {}

const header = [
'timestamp',
'cpu_user_ms',
'cpu_system_ms',
'cpuUserPercent',
'cpuSystemPercent',
'rss',
'heapTotal',
'heapUsed',
'heapUsedPercent',
].join(',') + '\n'

return fs.writeFile(FILE_PATH, header)
}

async function writeSample(data) {
const heapUsedPercent = data.memory.heapTotal > 0 ? (data.memory.heapUsed / data.memory.heapTotal) * 100 : 0
// cpu.user and cpu.system are now percent per core
const cpuUserPercent = data.cpu.user
const cpuSystemPercent = data.cpu.system
const line = [
data.timestamp,
data.cpu.user,
data.cpu.system,
cpuUserPercent,
cpuSystemPercent,
data.memory.rss,
data.memory.heapTotal,
data.memory.heapUsed,
heapUsedPercent,
].join(',') + '\n'
return fs.appendFile(FILE_PATH, line)
}

let running = false
async function main() {
// eslint-disable-next-line no-console
console.log(`Warming up for ${WARMUP_DURATION} ms...`)
await writeHeader()
setTimeout(() => {
running = true
const prevCpu = process.cpuUsage()
process.on('SIGINT', () => {
running = false
// eslint-disable-next-line no-console
console.log('Stopped monitoring.')
})

// eslint-disable-next-line no-console
console.log('Starting sampling')
sample({ prevCpu })
}, WARMUP_DURATION)
}

async function sample({ prevCpu }) {
const now = new Date().toISOString()
// Calculate CPU delta since last sample
const cpuUsage = getCpuUsage(prevCpu)
// Update prevCpu to the current snapshot for next interval
prevCpu = process.cpuUsage()
const memory = getMemoryUsage()
// CPU percent per core
const intervalMicros = SAMPLE_INTERVAL * 1000
const cpu = {
user: (cpuUsage.raw.user / intervalMicros) * 100,
system: (cpuUsage.raw.system / intervalMicros) * 100,
}
await writeSample({ timestamp: now, cpu, memory })
if (running) {
setTimeout(sample.bind(null, { prevCpu }), SAMPLE_INTERVAL)
}
}

main()
Loading