Skip to content

Commit 557ca4e

Browse files
atheurerclaude
andcommitted
Add deep dive view and 20x query performance improvement
Deep Dive view: - New DeepDiveView component with time-series line charts - "Dive" checkboxes on compare view metric panels to select metrics - Elapsed time X-axis with multiple iterations overlaid - Breakout configuration snapshot from compare view at transition - Progressive rendering as per-iteration data arrives - Adjustable resolution (default 100 data points) - New POST /api/v1/iterations/period-info endpoint for period data - Sequential metric loading to avoid OpenSearch contention Query performance (cdm.js): - Rewrite getMetricDataFromIdsSets with time-range templates built once per set and reused across all labels via string replace - Flush to OpenSearch every 10 labels instead of accumulating all queries before sending (was 104K array entries causing O(n) growth) - Replace then-request with native fetch (eliminates child process spawning via sync-rpc for every HTTP request) - Short-circuit numMBytes and memUsage when debug is off (was calling JSON.stringify on 104K-element arrays even with debug disabled) Server improvements: - Per-request IDs in server logs for correlating concurrent requests - Detailed metric-data logging with curl-reproducible commands - OpenSearch request/response timing at the HTTP level - npm ci with stamp files for efficient dependency management - Per-iteration sample selection support in supplemental-metric endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d53bed7 commit 557ca4e

8 files changed

Lines changed: 857 additions & 204 deletions

File tree

queries/cdmq/cdm.js

Lines changed: 205 additions & 136 deletions
Large diffs are not rendered by default.

queries/cdmq/server.js

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,32 @@ try {
2020
var logFile = logDir + '/cdm-server.log';
2121
var logStream = fs.createWriteStream(logFile, { flags: 'a' });
2222

23-
function serverLog(msg) {
24-
var line = '[' + new Date().toISOString() + '] ' + msg;
23+
function serverLog(msg, reqId) {
24+
var prefix = '[' + new Date().toISOString() + ']';
25+
if (reqId) prefix += ' [' + reqId + ']';
26+
var line = prefix + ' ' + msg;
2527
console.log(line);
2628
logStream.write(line + '\n');
2729
}
2830

29-
function serverError(msg) {
30-
var line = '[' + new Date().toISOString() + '] ERROR: ' + msg;
31+
function serverError(msg, reqId) {
32+
var prefix = '[' + new Date().toISOString() + ']';
33+
if (reqId) prefix += ' [' + reqId + ']';
34+
var line = prefix + ' ERROR: ' + msg;
3135
console.error(line);
3236
logStream.write(line + '\n');
3337
}
3438

39+
// Per-client request counter for generating short session-like IDs
40+
var clientCounters = {};
41+
function generateReqId(req) {
42+
var ip = req.ip || req.connection.remoteAddress || 'unknown';
43+
var shortIp = ip.replace(/^.*:/, ''); // last part of IPv6 or IPv4
44+
if (!clientCounters[shortIp]) clientCounters[shortIp] = 0;
45+
clientCounters[shortIp]++;
46+
return shortIp + '-' + clientCounters[shortIp];
47+
}
48+
3549
function save_host(host) {
3650
var host_info = { host: host, header: { 'Content-Type': 'application/json' } };
3751
instances.push(host_info);
@@ -87,6 +101,12 @@ serverLog('Instance info after discovery: ' + JSON.stringify(instances, null, 2)
87101
app.use(cors());
88102
app.use(express.json());
89103

104+
// Assign a request ID to each request for log correlation
105+
app.use(function (req, res, next) {
106+
req.reqId = generateReqId(req);
107+
next();
108+
});
109+
90110
// --------------------------------------------------------------------------------------------------------------
91111
// Middleware: resolve a run ID to an OpenSearch instance and yearDotMonth
92112
// Attaches req.cdm = { instance, yearDotMonth, runId } on success
@@ -1122,6 +1142,96 @@ app.post('/api/v1/iterations/breakout-values', async (req, res) => {
11221142
// Body: { runIds: [...], start, end, source, type, breakout: [...] }
11231143
// Returns: { values: { iterationId: { labels: { label: { mean, stddevPct, sampleValues } }, remainingBreakouts: [...] } } }
11241144
// When breakout is empty, returns a single label "__all__" with the aggregated value.
1145+
// --------------------------------------------------------------------------------------------------------------
1146+
// POST /api/v1/iterations/period-info — get period IDs and time ranges per iteration
1147+
// Body: { iterations: [{iterationId, runId}], start, end, sampleIndex }
1148+
// Returns: { periods: { iterationId: { periodId, begin, end, runId } } }
1149+
// --------------------------------------------------------------------------------------------------------------
1150+
app.post('/api/v1/iterations/period-info', async (req, res) => {
1151+
try {
1152+
const { iterations: reqIterations, start, end, sampleIndex } = req.body;
1153+
if (!Array.isArray(reqIterations) || reqIterations.length === 0) {
1154+
return res.status(400).json({ code: 'MISSING_PARAMS', error: 'iterations array is required' });
1155+
}
1156+
var requestedSampleIdx = (typeof sampleIndex === 'number') ? sampleIndex : null;
1157+
var perIterSampleIdx = (typeof sampleIndex === 'object' && sampleIndex !== null && !Array.isArray(sampleIndex)) ? sampleIndex : null;
1158+
1159+
getInstancesInfo(instances);
1160+
var result = {};
1161+
1162+
for (const inst of instances) {
1163+
if (invalidInstance(inst)) continue;
1164+
var ydm = cdm.buildYearDotMonthRange(inst, 'run', start || null, end || null);
1165+
1166+
var allIterIds = reqIterations.map(function (it) { return it.iterationId; });
1167+
var iterRunIds = reqIterations.map(function (it) { return it.runId; });
1168+
1169+
var samples = await cdm.mgetSamples(inst, allIterIds, ydm);
1170+
var statuses = await cdm.mgetSampleStatuses(inst, samples || [], ydm);
1171+
if (typeof statuses === 'undefined') statuses = [];
1172+
var periodNames = await cdm.mgetPrimaryPeriodName(inst, allIterIds, ydm);
1173+
1174+
var passingSamplesByIter = [];
1175+
var passingPeriodNamesByIter = [];
1176+
for (var i = 0; i < allIterIds.length; i++) {
1177+
var iterSamples = (samples && samples[i]) || [];
1178+
var iterStatuses = (statuses && statuses[i]) || [];
1179+
var iterPeriodName = (periodNames && periodNames[i]) || null;
1180+
var passing = [];
1181+
for (var s = 0; s < iterSamples.length; s++) {
1182+
if (iterStatuses[s] === 'pass') passing.push(iterSamples[s]);
1183+
}
1184+
passingSamplesByIter.push(passing);
1185+
passingPeriodNamesByIter.push(iterPeriodName);
1186+
}
1187+
1188+
var primaryPeriodIds = [];
1189+
var hasPassing = passingSamplesByIter.some(function (s) { return s.length > 0; });
1190+
if (hasPassing) {
1191+
primaryPeriodIds = await cdm.mgetPrimaryPeriodId(inst, passingSamplesByIter, passingPeriodNamesByIter, ydm);
1192+
if (typeof primaryPeriodIds === 'undefined') primaryPeriodIds = [];
1193+
}
1194+
1195+
var periodRanges = [];
1196+
if (primaryPeriodIds.length > 0) {
1197+
periodRanges = await cdm.mgetPeriodRange(inst, primaryPeriodIds, ydm);
1198+
if (typeof periodRanges === 'undefined') periodRanges = [];
1199+
}
1200+
1201+
for (var i = 0; i < allIterIds.length; i++) {
1202+
var iterPeriodIds = (primaryPeriodIds[i]) || [];
1203+
var iterRanges = (periodRanges[i]) || [];
1204+
if (iterPeriodIds.length === 0) continue;
1205+
1206+
var selIdx = 0;
1207+
if (perIterSampleIdx && perIterSampleIdx[allIterIds[i]] != null) {
1208+
selIdx = perIterSampleIdx[allIterIds[i]];
1209+
} else if (requestedSampleIdx !== null) {
1210+
selIdx = requestedSampleIdx;
1211+
}
1212+
if (selIdx >= iterPeriodIds.length) selIdx = 0;
1213+
1214+
if (!iterPeriodIds[selIdx]) continue;
1215+
var range = iterRanges[selIdx];
1216+
if (!range || !range.begin || !range.end) continue;
1217+
1218+
result[allIterIds[i]] = {
1219+
periodId: iterPeriodIds[selIdx],
1220+
begin: range.begin,
1221+
end: range.end,
1222+
runId: iterRunIds[i],
1223+
};
1224+
}
1225+
}
1226+
1227+
serverLog('POST /api/v1/iterations/period-info: ' + Object.keys(result).length + ' period(s)');
1228+
res.json({ periods: result });
1229+
} catch (error) {
1230+
serverError('Error in POST /api/v1/iterations/period-info: ' + error);
1231+
res.status(500).json({ code: 'INTERNAL_ERROR', error: 'Failed to get period info: ' + error.message });
1232+
}
1233+
});
1234+
11251235
// --------------------------------------------------------------------------------------------------------------
11261236
app.post('/api/v1/iterations/supplemental-metric', async (req, res) => {
11271237
try {
@@ -1448,18 +1558,10 @@ app.post('/api/v1/metric-data', async (req, res) => {
14481558
try {
14491559
var { run, period, begin, end, source, type, resolution, breakout, filter, instances: reqInstances } = req.body;
14501560

1451-
serverLog('[' + Date.now() + '] Fetching metric data with parameters:', {
1452-
run,
1453-
period,
1454-
begin,
1455-
end,
1456-
source,
1457-
type,
1458-
resolution,
1459-
breakout,
1460-
filter,
1461-
instances: reqInstances ? `${reqInstances.length} instance(s) provided` : 'using server instances'
1462-
});
1561+
var reqStart = Date.now();
1562+
var breakoutStr = Array.isArray(breakout) ? breakout.join(',') : (breakout || 'none');
1563+
serverLog('POST /api/v1/metric-data: ' + source + '::' + type + ' resolution=' + resolution + ' breakout=[' + breakoutStr + ']' + (filter ? ' filter=' + filter : '') + ' run=' + (run || 'none').toString().substring(0, 8) + '... period=' + (period || 'none').toString().substring(0, 8) + '...', req.reqId);
1564+
serverLog(' curl: curl -s -X POST http://localhost:3000/api/v1/metric-data -H "Content-Type: application/json" -d \'' + JSON.stringify({ run: run, period: period, begin: begin, end: end, source: source, type: type, resolution: resolution, breakout: breakout, filter: filter }) + '\'', req.reqId);
14631565

14641566
// Use instances from request if provided, otherwise use server's configured instances
14651567
var instancesToUse = reqInstances && reqInstances.length > 0 ? reqInstances : instances;
@@ -1537,15 +1639,9 @@ app.post('/api/v1/metric-data', async (req, res) => {
15371639
}
15381640
metric_data = resp['data-sets'][0];
15391641

1540-
console.log(
1541-
'[' +
1542-
Date.now() +
1543-
'] Request completed from Opensearch instance: ' +
1544-
instance['host'] +
1545-
' and cdm: ' +
1546-
instance['ver'] +
1547-
'\n'
1548-
);
1642+
var labelCount = metric_data && metric_data.values ? Object.keys(metric_data.values).length : 0;
1643+
var elapsed = Date.now() - reqStart;
1644+
serverLog('POST /api/v1/metric-data: ' + source + '::' + type + ' -> ' + labelCount + ' label(s) in ' + elapsed + 'ms', req.reqId);
15491645

15501646
// Return the data
15511647
res.json(metric_data);

queries/cdmq/start-server.sh

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,42 @@ if ! command -v npm >/dev/null 2>&1; then
2121
popd >/dev/null
2222
exit 1
2323
fi
24-
echo "Resolving cdmq dependencies..."
25-
npm install --no-fund --no-audit 2>&1 | tail -1
24+
# Install dependencies only when package-lock.json is newer than last install
25+
if [ ! -f "node_modules/.install-stamp" ] || [ "package-lock.json" -nt "node_modules/.install-stamp" ]; then
26+
echo "Installing cdmq dependencies..."
27+
npm ci --no-fund --no-audit 2>&1 | tail -1
28+
touch node_modules/.install-stamp
29+
else
30+
echo "cdmq dependencies up to date"
31+
fi
2632

2733
# Build the web UI if source exists
2834
if [ -d "web-ui" ] && [ -f "web-ui/package.json" ]; then
29-
echo "Building web UI..."
3035
pushd web-ui >/dev/null
31-
npm install --no-fund --no-audit 2>&1 | tail -1
32-
node node_modules/.bin/vite build 2>&1
33-
build_rc=$?
34-
popd >/dev/null
35-
if [ $build_rc -ne 0 ]; then
36-
echo "Warning: web UI build failed (rc=$build_rc), server will start without UI"
36+
if [ ! -f "node_modules/.install-stamp" ] || [ "package-lock.json" -nt "node_modules/.install-stamp" ]; then
37+
echo "Installing web UI dependencies..."
38+
npm ci --no-fund --no-audit 2>&1 | tail -1
39+
touch node_modules/.install-stamp
40+
fi
41+
# Rebuild if any source file is newer than the dist
42+
if [ ! -d "dist" ] || [ -n "$(find src -newer dist/index.html 2>/dev/null | head -1)" ] || [ "package-lock.json" -nt "dist/index.html" ]; then
43+
echo "Building web UI..."
44+
node node_modules/.bin/vite build 2>&1
45+
build_rc=$?
46+
if [ $build_rc -ne 0 ]; then
47+
echo "Warning: web UI build failed (rc=$build_rc), server will start without UI"
48+
else
49+
echo "Web UI built successfully"
50+
fi
3751
else
38-
echo "Web UI built successfully"
52+
echo "Web UI build up to date"
3953
fi
54+
popd >/dev/null
4055
fi
4156

4257
while true; do
4358
echo "Starting server.js..."
59+
#CDM_LOG_OS_CURL=1 node ./server.js "$@"
4460
node ./server.js "$@"
4561
rc=$?
4662
echo "server.js exited with rc=$rc, restarting..."

queries/cdmq/web-ui/src/App.jsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SearchPanel from './components/SearchPanel';
33
import SelectionBar from './components/SelectionBar';
44
import IterationTable from './components/IterationTable';
55
import CompareView from './components/CompareView';
6+
import DeepDiveView from './components/DeepDiveView';
67
import DebugConsole from './components/DebugConsole';
78
import './index.css';
89

@@ -67,6 +68,8 @@ export default function App() {
6768
const lastFilters = useRef(null);
6869
const restoredState = useRef(null);
6970
const [restoredMetrics, setRestoredMetrics] = useState(null);
71+
const [deepDiveMetrics, setDeepDiveMetrics] = useState(new Set()); // Set of "source::type" strings
72+
const [deepDiveConfigs, setDeepDiveConfigs] = useState([]); // snapshot of supplemental metrics for deep dive
7073

7174
// On mount, check for state in URL hash
7275
// Don't switch view yet — wait until search completes and selections are applied
@@ -207,10 +210,16 @@ export default function App() {
207210
</button>
208211
<button
209212
className={view === 'deepdive' ? 'active' : ''}
210-
onClick={() => setView('deepdive')}
211-
disabled={selected.size === 0}
213+
onClick={() => {
214+
// Snapshot supplemental metric configs before CompareView unmounts
215+
if (compareRef.current) {
216+
setDeepDiveConfigs(compareRef.current.getSupplementalMetrics() || []);
217+
}
218+
setView('deepdive');
219+
}}
220+
disabled={selected.size === 0 || deepDiveMetrics.size === 0}
212221
>
213-
Deep Dive
222+
Deep Dive{deepDiveMetrics.size > 0 ? ' (' + deepDiveMetrics.size + ')' : ''}
214223
</button>
215224
</nav>
216225
</div>
@@ -246,11 +255,11 @@ export default function App() {
246255
)}
247256

248257
{view === 'compare' && (
249-
<CompareView ref={compareRef} selected={selected} groupByList={groupByList} setGroupByList={setGroupByList} hiddenFields={hiddenFields} setHiddenFields={setHiddenFields} restoredMetrics={restoredMetrics} />
258+
<CompareView ref={compareRef} selected={selected} groupByList={groupByList} setGroupByList={setGroupByList} hiddenFields={hiddenFields} setHiddenFields={setHiddenFields} restoredMetrics={restoredMetrics} deepDiveMetrics={deepDiveMetrics} setDeepDiveMetrics={setDeepDiveMetrics} />
250259
)}
251260

252261
{view === 'deepdive' && (
253-
<div className="empty-msg">Phase 3: Time-series deep dive coming soon.</div>
262+
<DeepDiveView selected={selected} deepDiveMetrics={deepDiveMetrics} metricConfigs={deepDiveConfigs} />
254263
)}
255264

256265
<DebugConsole />

queries/cdmq/web-ui/src/api/cdm.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ export async function getBreakoutValues(params) {
140140
});
141141
}
142142

143+
export async function getPeriodInfo(params) {
144+
return request('POST', '/iterations/period-info', {
145+
iterations: params.iterations,
146+
start: params.start,
147+
end: params.end,
148+
sampleIndex: params.sampleIndex,
149+
});
150+
}
151+
143152
export async function getMetricData(params) {
144153
return request('POST', `/metric-data`, params);
145154
}

0 commit comments

Comments
 (0)