From 02168f4cfdca0cd7d19da1a49b4edc5205cbb052 Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 07:18:10 +0000 Subject: [PATCH 1/6] test network debugger --- src/InspectorHTTPServer.cpp | 22 ++++++++++ src/jsc/JavaScriptContext.cpp | 70 +++++++++++++++++++++++++++++-- src/jsc/JavaScriptUtils.cpp | 79 +++-------------------------------- 3 files changed, 94 insertions(+), 77 deletions(-) diff --git a/src/InspectorHTTPServer.cpp b/src/InspectorHTTPServer.cpp index aae892d..e358a78 100644 --- a/src/InspectorHTTPServer.cpp +++ b/src/InspectorHTTPServer.cpp @@ -738,4 +738,26 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, } +void InspectorHTTPServer::sendNetworkMetric(JSContextRef context, const char* paramsJson) +{ + if (!context || !paramsJson) return; + + JSGlobalContextRef globalContext = const_cast(context); + + SoupWebsocketConnection* targetConnection = nullptr; + for (const auto& pair : m_connections) { + if (pair.second == globalContext) { + targetConnection = pair.first; + break; + } + } + + if (!targetConnection) return; + if (soup_websocket_connection_get_state(targetConnection) != SOUP_WEBSOCKET_STATE_OPEN) return; + + std::ostringstream event; + event << "{\"method\":\"JSRuntime.networkMetric\",\"params\":" << paramsJson << "}"; + std::string eventStr = event.str(); + soup_websocket_connection_send_text(targetConnection, eventStr.c_str()); +} #endif // REMOTE_INSPECTOR_ENABLE diff --git a/src/jsc/JavaScriptContext.cpp b/src/jsc/JavaScriptContext.cpp index 5779bcf..e2cdfc0 100644 --- a/src/jsc/JavaScriptContext.cpp +++ b/src/jsc/JavaScriptContext.cpp @@ -591,16 +591,74 @@ if (mModuleSettings.enablePlayer) } } +static std::string jsonEscape(const char* str) +{ + std::string out; + if (!str) return out; + static const char* hex = "0123456789abcdef"; + for (const unsigned char* p = reinterpret_cast(str); *p; ++p) { + switch (*p) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\b': out += "\\b"; break; + case '\f': out += "\\f"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (*p < 0x20) { + out += "\\u00"; + out += hex[(*p >> 4) & 0x0f]; + out += hex[*p & 0x0f]; + } else { + out += static_cast(*p); + } + } + } + return out; +} + void JavaScriptContext::onMetricsData (NetworkMetrics *net) { - if (!net) { + if (!net) { rtLogError("onMetricsData: Received null NetworkMetrics structure."); return; } - rtString key = net->url; - mNetworkMetricsData->set(key, rtValue((void *)net)); + + std::lock_guard lock(mNetworkMetricsMutex); + if (!mNetworkCaptureEnabled) { + // Recording is off (inspector Network tab inactive) — drop without storing. + delete net; + return; + } + + // Stream this request to the inspector (console-log style) and keep nothing. + std::ostringstream params; + params << "{\"url\":\"" << jsonEscape(net->url.cString()) << "\"," + << "\"method\":\"" << jsonEscape(net->method.cString()) << "\"," + << "\"statusCode\":" << net->statusCode << "," + << "\"headers\":["; + for (size_t j = 0; j < net->headers.size(); ++j) { + if (j) params << ","; + params << "\"" << jsonEscape(net->headers[j].cString()) << "\""; + } + params << "],\"timeMetricsData\":["; + bool firstMetric = true; + for (const auto& metric : net->timeMetricsData) { + if (!firstMetric) params << ","; + firstMetric = false; + params << "{\"" << jsonEscape(metric.first.cString()) << "\":\"" + << jsonEscape(metric.second.toString().cString()) << "\"}"; + } + params << "]}"; + + #ifdef REMOTE_INSPECTOR_ENABLE + InspectorHTTPServer::singleton().sendNetworkMetric(mContext, params.str().c_str()); + #endif dumpNetworkMetricData(net, this->getUrl()); + delete net; + net = nullptr; } void JavaScriptContext::dumpNetworkMetricData(NetworkMetrics *metrics, std::string appUrl) @@ -688,3 +746,9 @@ void JavaScriptContext::handleExternalApplication(const std::string& url) mExternalApplicationHandler->runExternalApplication(url, mIds); } } + +void JavaScriptContext::setNetworkCaptureEnabled(bool enabled) +{ + std::lock_guard lock(mNetworkMetricsMutex); + mNetworkCaptureEnabled = enabled; +} diff --git a/src/jsc/JavaScriptUtils.cpp b/src/jsc/JavaScriptUtils.cpp index b98e77b..b6c6130 100644 --- a/src/jsc/JavaScriptUtils.cpp +++ b/src/jsc/JavaScriptUtils.cpp @@ -70,8 +70,7 @@ std::thread::id gMainThreadId; static std::list> gPendingFun; static std::mutex gDispatchMutex; -static const char* envValue = std::getenv("NATIVEJS_DUMP_NETWORKMETRIC"); - +bool enabled = std::getenv("NATIVEJS_DUMP_NETWORKMETRIC") != nullptr || std::getenv("ETHAN_LOGGING_PIPE") != nullptr; void TimeoutQueue::pushTimeouts(const std::vector& timerVec) { if (!timerVec.size()) @@ -243,8 +242,6 @@ void assertIsMainThread() } mMetricsListener->onMetricsData(metrics); - delete metrics; - metrics = nullptr; } rtObjectRef protectedRef = resp; @@ -722,79 +719,13 @@ rtError rtJSRuntimeDownloadMetrics(int numArgs, const rtValue* args, rtValue* re { JavaScriptContext* jscContext = (JavaScriptContext*)context; if (jscContext == nullptr) { - rtLogError("context null"); - return RT_FAIL; - } - - rtMapObject* map = jscContext->getNetworkMetricsData(); - if (!map) { - return RT_FAIL; - } - - rtArrayObject* netMetricsArray = new rtArrayObject(); - - rtValue keys; - if (map->Get("allKeys", &keys) != RT_OK) { - rtLogWarn("Could not retrieve url for network metrics data."); - delete netMetricsArray; - return RT_FAIL; - } - rtObjectRef objRef = keys.toObject(); - rtArrayObject* keysArray = static_cast(objRef.getPtr()); - - if (!keysArray) { - rtLogWarn("No url found in the network metrics data."); - delete netMetricsArray; + rtLogError("%s: context null", __FUNCTION__); return RT_FAIL; } - size_t count = keysArray->length(); - - for (size_t i = 0; i < count; ++i) { - rtValue keyValue; - rtString key = keysArray->get(i); - - keysArray->Get(key, &keyValue); - rtValue storedValue; - if (map->Get(key.cString(), &storedValue) == RT_OK) { - NetworkMetrics* metrics = (NetworkMetrics*)storedValue.toVoidPtr(); - if (!metrics) { - rtLogError("Failed to cast stored value to NetworkMetrics structure for url: %s.", key.cString()); - delete netMetricsArray; - return RT_FAIL; - } - rtMapObject* metricsMap = new rtMapObject(); - metricsMap->set("url", metrics->url); - metricsMap->set("method", metrics->method); - - rtArrayObject* headersArray = new rtArrayObject(); - for (const auto& header : metrics->headers) { - headersArray->pushBack(rtValue(header)); - } - metricsMap->set("headers", rtValue(headersArray)); - delete headersArray; - - rtArrayObject* timeMetricsArray = new rtArrayObject(); - for (const auto& metric : metrics->timeMetricsData) { - rtObjectRef timeMetricObj = new rtMapObject(); - timeMetricObj->Set(metric.first.cString(), &metric.second); - timeMetricsArray->pushBack(rtValue(timeMetricObj)); - delete timeMetricObj; - timeMetricObj = nullptr; - } - - metricsMap->set("timeMetricsData", rtValue(timeMetricsArray)); - delete timeMetricsArray; - - netMetricsArray->pushBack(rtValue(metricsMap)); - } - else { - rtLogWarn("Url not found in network metrics data: %s", key.cString()); - } - } - - *result = rtValue(netMetricsArray); - + bool enabled = (numArgs >= 1) ? args[0].toBool() : false; + jscContext->setNetworkCaptureEnabled(enabled); + if (result) *result = true; return RT_OK; } From 44e61d9aa917db1cc187b2e954891dc6757a1c43 Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 08:51:50 +0000 Subject: [PATCH 2/6] test --- include/InspectorHTTPServer.h | 2 + include/jsc/JavaScriptContext.h | 3 + utils/nativejsinspector.html | 371 +++++++++++++++++++++++++++++++- 3 files changed, 374 insertions(+), 2 deletions(-) diff --git a/include/InspectorHTTPServer.h b/include/InspectorHTTPServer.h index 68a1bb0..6bcd69a 100644 --- a/include/InspectorHTTPServer.h +++ b/include/InspectorHTTPServer.h @@ -44,6 +44,8 @@ class InspectorHTTPServer { void sendConsoleMessage(JSContextRef context, const char* level, const char* text); + void sendNetworkMetric(JSContextRef context, const char* paramsJson); + void registerScript(const char* url, const char* source); // Called when frontend sends Page.reload. diff --git a/include/jsc/JavaScriptContext.h b/include/jsc/JavaScriptContext.h index 3f4fc60..2f188f6 100644 --- a/include/jsc/JavaScriptContext.h +++ b/include/jsc/JavaScriptContext.h @@ -83,6 +83,7 @@ class JavaScriptContext: public JavaScriptContextBase, public NetworkMetricsList virtual void onMetricsData (NetworkMetrics *net) override; rtMapObject* getNetworkMetricsData() const { return mNetworkMetricsData; } void dumpNetworkMetricData(NetworkMetrics *metrics, std::string appUrl); + void setNetworkCaptureEnabled(bool enabled); void setCreateApplicationStartTime(double time); void setCreateApplicationEndTime(double time,uint32_t id); @@ -116,6 +117,8 @@ class JavaScriptContext: public JavaScriptContextBase, public NetworkMetricsList JSGlobalContextRef mContext; PerformanceMetrics mPerformanceMetrics; rtMapObject* mNetworkMetricsData; + std::mutex mNetworkMetricsMutex; + bool mNetworkCaptureEnabled = false; rtRef mPriv; rtRef m_webSocketBinding; rtRef m_webSocketServerBinding; diff --git a/utils/nativejsinspector.html b/utils/nativejsinspector.html index 7392cac..3354efc 100644 --- a/utils/nativejsinspector.html +++ b/utils/nativejsinspector.html @@ -54,6 +54,24 @@ #clear-btn { background: #d0d0d0; border: none; color: #1e1e1e; padding: 4px 12px; border-radius: 3px; cursor: pointer; font-size: 11px; } #clear-btn:hover { background: #b8b8b8; } + /* Network Panel */ + #network-panel { display: none; flex-direction: column; height: 100%; } + #network-panel.active { display: flex; } + #network-toolbar { background: #e8e8e8; padding: 6px 8px; border-bottom: 1px solid #c8c8c8; display: flex; align-items: center; gap: 8px; } + #network-toolbar button { background: #d0d0d0; border: 1px solid #b0b0b0; color: #1e1e1e; padding: 4px 10px; border-radius: 3px; cursor: pointer; font-size: 11px; } + #network-output { flex: 1; overflow: auto; background: #fafafa; font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; } + .network-table { width: 100%; border-collapse: collapse; background: #fff; } + .network-table th, .network-table td { border-bottom: 1px solid #e5e5e5; padding: 8px 10px; text-align: left; vertical-align: top; } + .network-table th { background: #f0f0f0; color: #333; font-size: 11px; font-weight: 600; position: sticky; top: 0; z-index: 1; } + .network-table td { color: #1e1e1e; font-size: 12px; } + .network-table tr:hover { background: #f9fbff; } + .name-cell { min-width: 220px; word-break: break-all; } + .domain-cell { color: #555; } + .network-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 10px; color: #666; min-height: 180px; } + .network-empty .placeholder-icon { font-size: 28px; opacity: 0.5; } + .network-empty .placeholder-title { font-size: 14px; color: #333; font-weight: 600; } + .network-empty .placeholder-hint { font-size: 11px; line-height: 1.5; } + /* Sources Panel */ #sources-panel { flex-direction: row !important; } #sources-sidebar { width: 280px; background: #ebebeb; border-right: 1px solid #c8c8c8; overflow-y: auto; } @@ -103,6 +121,7 @@

RDK NativeScript Inspector

Console
+
Network
Sources
@@ -124,6 +143,17 @@

RDK NativeScript Inspector

+ +
+
+ + Live network metrics stream while this tab is active +
+
+
Recording starts when this tab is open. Network requests will appear here.
+
+
+
@@ -162,7 +192,8 @@

RDK NativeScript Inspector

contextId: 1, scripts: new Map(), currentScript: null, - pendingCallbacks: new Map() + pendingCallbacks: new Map(), + networkEntries: [] }; // DOM elements @@ -181,7 +212,9 @@

RDK NativeScript Inspector

document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); tab.classList.add('active'); $(tab.dataset.panel + '-panel').classList.add('active'); - + if (tab.dataset.panel === 'network') { + renderNetworkMetrics(state.networkEntries); + } }); }); @@ -261,6 +294,11 @@

RDK NativeScript Inspector

send({ method: 'Runtime.enable' }); send({ method: 'Console.enable' }); send({ method: 'Debugger.enable' }); + // Keep network capture enabled for the whole inspector session, + // similar to console logging. + setNetworkCapture(true); + const netActive = $('network-panel') && $('network-panel').classList.contains('active'); + if (netActive) renderNetworkMetrics(state.networkEntries); loadInitialScripts(); }; @@ -307,6 +345,19 @@

RDK NativeScript Inspector

const text = args.map(arg => arg.value || '').join(' '); log(text, type); } + else if (msg.method === 'JSRuntime.networkMetric') { + // Streamed request (console-log style) — accumulate locally, never stored on the runtime. + const metric = msg.params || {}; + state.networkEntries.push(metric); + const MAX_ENTRIES = 2000; + if (state.networkEntries.length > MAX_ENTRIES) { + state.networkEntries.splice(0, state.networkEntries.length - MAX_ENTRIES); + } + const panel = $('network-panel'); + if (panel && panel.classList.contains('active')) { + renderNetworkMetrics(state.networkEntries); + } + } else if (msg.method === 'Runtime.executionContextCreated') { log(`Context created (id: ${msg.params.context.id})`, 'debug'); } @@ -355,6 +406,317 @@

RDK NativeScript Inspector

} // Sources panel + function extractMetricValue(metrics, key) { + if (!Array.isArray(metrics)) return null; + for (const item of metrics) { + if (item && typeof item === 'object' && Object.prototype.hasOwnProperty.call(item, key)) { + return item[key]; + } + } + return null; + } + + function extractHeaderValue(headers, key) { + if (!Array.isArray(headers)) return null; + for (const header of headers) { + if (typeof header === 'string' && header.toLowerCase().startsWith(key.toLowerCase() + ':')) { + return header.split(':').slice(1).join(':').trim(); + } + } + return null; + } + + function getRequestType(url) { + if (!url) return 'Other'; + try { + const pathname = new URL(url).pathname.toLowerCase(); + if (pathname.endsWith('.js') || pathname.includes('script')) return 'Script'; + if (pathname.endsWith('.css') || pathname.includes('style')) return 'Stylesheet'; + if (pathname.endsWith('.png') || pathname.endsWith('.jpg') || pathname.endsWith('.jpeg') || pathname.endsWith('.gif') || pathname.endsWith('.webp')) return 'Image'; + if (pathname.endsWith('.woff') || pathname.endsWith('.ttf') || pathname.endsWith('.otf')) return 'Font'; + if (pathname.includes('/api/') || pathname.includes('/xhr') || pathname.includes('/fetch')) return 'XHR'; + return 'Document'; + } catch (e) { + return 'XHR'; + } + } + + function getRequestSummary(entry) { + const url = entry && entry.url ? String(entry.url) : ''; + const domain = (() => { + try { return new URL(url).hostname || ''; } catch (e) { return ''; } + })(); + const method = (entry && entry.method) ? String(entry.method).toUpperCase() : 'GET'; + const status = entry && entry.statusCode !== undefined ? Number(entry.statusCode) : 0; + const statusText = Number.isFinite(status) && status > 0 ? String(status) : '--'; + const timeValue = extractMetricValue(entry && entry.timeMetricsData, 'totalDownloadTimeMs'); + const connectValue = extractMetricValue(entry && entry.timeMetricsData, 'connectTimeMs'); + const time = (timeValue !== null && timeValue !== undefined) ? `${timeValue} ms` : (connectValue !== null && connectValue !== undefined ? `${connectValue} ms` : 'n/a'); + const transferBytes = extractHeaderValue(entry && entry.headers, 'Content-Length'); + const size = transferBytes ? `${transferBytes} B` : ( + (extractMetricValue(entry && entry.timeMetricsData, 'downloadSpeedBytesPerSecond') !== null && extractMetricValue(entry && entry.timeMetricsData, 'downloadSpeedBytesPerSecond') !== undefined && timeValue !== null && timeValue !== undefined) + ? `${Math.max(1, Math.round((extractMetricValue(entry && entry.timeMetricsData, 'downloadSpeedBytesPerSecond') * Number(timeValue)) / 1000))} B` + : 'n/a' + ); + const type = getRequestType(url); + const initiator = method === 'GET' || method === 'POST' ? 'xhr' : 'script'; + const name = (() => { + if (!url) return 'Unknown'; + try { return new URL(url).pathname || url; } catch (e) { return url; } + })(); + + return { + name, + domain, + method, + status, + statusText, + type, + time, + size, + initiator + }; + } + + function renderNetworkMetrics(metrics) { + const output = $('network-output'); + if (!output) return; + + if (!Array.isArray(metrics) || metrics.length === 0) { + output.innerHTML = ` +
+
⏳
+
No network requests yet
+
The Network tab will show browser-like request details as calls complete.
+
+ `; + return; + } + + const rows = metrics.map((entry) => { + const summary = getRequestSummary(entry); + + return ` + + ${escapeHtml(summary.name)} + ${escapeHtml(summary.domain || 'n/a')} + ${escapeHtml(summary.size)} + ${escapeHtml(summary.type)} + ${escapeHtml(summary.time)} + ${escapeHtml(summary.initiator)} + + `; + }).join(''); + + output.innerHTML = ` + + + + + + + + + + + + ${rows} +
NameDomainTransfer sizeTypeTimeInitiator
+ `; + } + + // Keep native recording enabled while the inspector session is connected. + function setNetworkCapture(enabled) { + if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return; + send({ + method: 'Runtime.evaluate', + params: { + expression: '(typeof JSRuntimeDownloadMetrics === "function") ? JSRuntimeDownloadMetrics(' + (enabled ? 'true' : 'false') + ') : false', + returnByValue: true + } + }); + } + + // Drop all captured requests and start fresh (browser-like refresh). + function clearNetworkCapture() { + state.networkEntries = []; + const output = $('network-output'); + if (output) { + output.innerHTML = '
Recording… network requests will appear here.
'; + } + } + + // ── HAR 1.2 export (client-side, built from streamed network entries) ────────────── + function harFlattenTimings(timeMetricsData) { + const flat = {}; + if (!Array.isArray(timeMetricsData)) return flat; + for (const entry of timeMetricsData) { + if (entry && typeof entry === 'object') { + for (const k in entry) { + if (Object.prototype.hasOwnProperty.call(entry, k)) flat[k] = entry[k]; + } + } + } + return flat; + } + + function harToNumber(value) { + const n = typeof value === 'number' ? value : parseFloat(value); + return isFinite(n) ? n : -1; + } + + // Values may be seconds or ms; normalize to ms. + function harToMillis(value) { + const n = harToNumber(value); + if (n < 0) return -1; + return n < 100 ? n * 1000 : n; + } + + function harPickTiming(flat, keys) { + for (const key of keys) { + if (flat[key] !== undefined) return harToMillis(flat[key]); + } + return -1; + } + + function harParseHeaders(headers) { + const out = []; + if (!Array.isArray(headers)) return out; + for (const raw of headers) { + if (raw && typeof raw === 'object' && raw.name !== undefined) { + out.push({ name: String(raw.name), value: String(raw.value === undefined ? '' : raw.value) }); + continue; + } + const str = String(raw === undefined ? '' : raw); + const idx = str.indexOf(':'); + if (idx >= 0) { + out.push({ name: str.slice(0, idx).trim(), value: str.slice(idx + 1).trim() }); + } else if (str.length) { + out.push({ name: str.trim(), value: '' }); + } + } + return out; + } + + function harParseQueryString(url) { + const out = []; + const q = String(url || '').indexOf('?'); + if (q < 0) return out; + let query = String(url).slice(q + 1); + const hash = query.indexOf('#'); + if (hash >= 0) query = query.slice(0, hash); + if (!query.length) return out; + for (const pair of query.split('&')) { + if (!pair.length) continue; + const eq = pair.indexOf('='); + const name = eq >= 0 ? pair.slice(0, eq) : pair; + const value = eq >= 0 ? pair.slice(eq + 1) : ''; + let dn = name, dv = value; + try { dn = decodeURIComponent(name); } catch (e) { dn = name; } + try { dv = decodeURIComponent(value); } catch (e2) { dv = value; } + out.push({ name: dn, value: dv }); + } + return out; + } + + function harBuildTimings(flat) { + return { + blocked: -1, + dns: harPickTiming(flat, ['dns', 'dnsTime', 'dnsTimeMs', 'nameLookupTime', 'namelookup']), + connect: harPickTiming(flat, ['connect', 'connectTime', 'connectTimeMs', 'connecttime']), + ssl: harPickTiming(flat, ['ssl', 'sslTime', 'sslTimeMs', 'appConnectTime', 'appconnect']), + send: (() => { const v = harPickTiming(flat, ['send', 'sendTime', 'sendTimeMs']); return v >= 0 ? v : 0; })(), + wait: (() => { const v = harPickTiming(flat, ['wait', 'waitTime', 'startTransferTime', 'starttransfer', 'ttfb']); return v >= 0 ? v : 0; })(), + receive: (() => { const v = harPickTiming(flat, ['receive', 'receiveTime', 'totalDownloadTimeMs']); return v >= 0 ? v : 0; })() + }; + } + + function harBuildEntry(metric) { + const url = metric && metric.url !== undefined ? String(metric.url) : ''; + const method = metric && metric.method !== undefined ? String(metric.method) : 'GET'; + const status = metric && metric.statusCode !== undefined ? harToNumber(metric.statusCode) : 0; + const flat = harFlattenTimings(metric ? metric.timeMetricsData : null); + const timings = harBuildTimings(flat); + + let total = harPickTiming(flat, ['total', 'totalTime', 'totalDownloadTimeMs', 'totaltime']); + if (total < 0) { + let sum = 0, any = false; + for (const part of ['dns', 'connect', 'ssl', 'send', 'wait', 'receive']) { + if (timings[part] >= 0) { sum += timings[part]; any = true; } + } + total = any ? sum : 0; + } + + return { + startedDateTime: new Date().toISOString(), + time: total, + request: { + method: method, + url: url, + httpVersion: 'HTTP/1.1', + cookies: [], + headers: harParseHeaders(metric ? metric.headers : null), + queryString: harParseQueryString(url), + headersSize: -1, + bodySize: -1 + }, + response: { + status: status, + statusText: '', + httpVersion: 'HTTP/1.1', + cookies: [], + headers: [], + content: { size: 0, mimeType: '' }, + redirectURL: '', + headersSize: -1, + bodySize: -1 + }, + cache: {}, + timings: timings + }; + } + + function buildHarFromMetrics(metrics) { + const entries = Array.isArray(metrics) ? metrics.map(harBuildEntry) : []; + return { + log: { + version: '1.2', + creator: { name: 'rdkNativeScript-Inspector', version: '1.0' }, + pages: [], + entries: entries + } + }; + } + + function downloadHarFile(harObject) { + const text = JSON.stringify(harObject, null, 2); + const blob = new Blob([text], { type: 'application/json' }); + const objectUrl = URL.createObjectURL(blob); + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const a = document.createElement('a'); + a.href = objectUrl; + a.download = `jsruntime-${stamp}.har`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); + } + + function exportHAR() { + const btn = $('network-export-har-btn'); + if (btn) { btn.disabled = true; btn.textContent = 'Exporting…'; } + try { + const har = buildHarFromMetrics(state.networkEntries); + const count = har.log.entries.length; + downloadHarFile(har); + log(`Exported HAR with ${count} entr${count === 1 ? 'y' : 'ies'}`, 'info'); + } catch (e) { + log('HAR export error: ' + e.message, 'error'); + } finally { + if (btn) { btn.disabled = false; btn.textContent = 'Export HAR'; } + } + } + function toggleFolder(element) { element.classList.toggle('open'); } @@ -553,6 +915,9 @@

RDK NativeScript Inspector

lastLogEntry = null; renderFileTree(); + // Browser-like refresh: drop all captured network data and start fresh. + clearNetworkCapture(); + // Ask the runtime to reload via Page.reload (CDP). // The server re-executes the app and pushes fresh Debugger.scriptParsed // events over the existing WebSocket — no reconnect needed. @@ -565,6 +930,7 @@

RDK NativeScript Inspector

} $('reload-btn').addEventListener('click', reloadContext); + $('network-export-har-btn').addEventListener('click', exportHAR); // Initialize connect(); @@ -572,3 +938,4 @@

RDK NativeScript Inspector

+ From 04cb9599ee084aae743bda7cf9fb727f5c0a1e5c Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 09:09:29 +0000 Subject: [PATCH 3/6] test --- src/jsc/JavaScriptUtils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/JavaScriptUtils.cpp b/src/jsc/JavaScriptUtils.cpp index b6c6130..1346c41 100644 --- a/src/jsc/JavaScriptUtils.cpp +++ b/src/jsc/JavaScriptUtils.cpp @@ -70,7 +70,7 @@ std::thread::id gMainThreadId; static std::list> gPendingFun; static std::mutex gDispatchMutex; -bool enabled = std::getenv("NATIVEJS_DUMP_NETWORKMETRIC") != nullptr || std::getenv("ETHAN_LOGGING_PIPE") != nullptr; +bool envValue = std::getenv("NATIVEJS_DUMP_NETWORKMETRIC") != nullptr || std::getenv("ETHAN_LOGGING_PIPE") != nullptr; void TimeoutQueue::pushTimeouts(const std::vector& timerVec) { if (!timerVec.size()) From 45be903d19641cd439d8d616ff2102bed9ac7d5a Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 10:37:06 +0000 Subject: [PATCH 4/6] test --- src/jsc/JavaScriptContext.cpp | 66 +++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/jsc/JavaScriptContext.cpp b/src/jsc/JavaScriptContext.cpp index e2cdfc0..03983cd 100644 --- a/src/jsc/JavaScriptContext.cpp +++ b/src/jsc/JavaScriptContext.cpp @@ -591,6 +591,7 @@ if (mModuleSettings.enablePlayer) } } + static std::string jsonEscape(const char* str) { std::string out; @@ -618,6 +619,39 @@ static std::string jsonEscape(const char* str) return out; } +static std::string buildNetworkMetricPayload(const NetworkMetrics& net) +{ + std::ostringstream params; + params << "{\"url\":\"" << jsonEscape(net.url.cString()) << "\"," + << "\"method\":\"" << jsonEscape(net.method.cString()) << "\"," + << "\"statusCode\":" << net.statusCode << "," + << "\"headers\":["; + + for (size_t j = 0; j < net.headers.size(); ++j) { + if (j) { + params << ","; + } + params << "\"" << jsonEscape(net.headers[j].cString()) << "\""; + } + + params << "],\"timeMetricsData\":["; + bool firstMetric = true; + for (const auto& metric : net.timeMetricsData) { + if (!firstMetric) { + params << ","; + } + firstMetric = false; + + const rtString metricKey = metric.first; + const rtString metricValue = metric.second.toString(); + params << "{\"" << jsonEscape(metricKey.cString()) << "\":\"" + << jsonEscape(metricValue.cString()) << "\"}"; + } + + params << "]}"; + return params.str(); +} + void JavaScriptContext::onMetricsData (NetworkMetrics *net) { if (!net) { @@ -625,35 +659,21 @@ void JavaScriptContext::onMetricsData (NetworkMetrics *net) return; } - std::lock_guard lock(mNetworkMetricsMutex); - if (!mNetworkCaptureEnabled) { - // Recording is off (inspector Network tab inactive) — drop without storing. + bool networkCaptureEnabled = false; + { + std::lock_guard lock(mNetworkMetricsMutex); + networkCaptureEnabled = mNetworkCaptureEnabled; + } + + if (!networkCaptureEnabled) { delete net; return; } - // Stream this request to the inspector (console-log style) and keep nothing. - std::ostringstream params; - params << "{\"url\":\"" << jsonEscape(net->url.cString()) << "\"," - << "\"method\":\"" << jsonEscape(net->method.cString()) << "\"," - << "\"statusCode\":" << net->statusCode << "," - << "\"headers\":["; - for (size_t j = 0; j < net->headers.size(); ++j) { - if (j) params << ","; - params << "\"" << jsonEscape(net->headers[j].cString()) << "\""; - } - params << "],\"timeMetricsData\":["; - bool firstMetric = true; - for (const auto& metric : net->timeMetricsData) { - if (!firstMetric) params << ","; - firstMetric = false; - params << "{\"" << jsonEscape(metric.first.cString()) << "\":\"" - << jsonEscape(metric.second.toString().cString()) << "\"}"; - } - params << "]}"; + const std::string params = buildNetworkMetricPayload(*net); #ifdef REMOTE_INSPECTOR_ENABLE - InspectorHTTPServer::singleton().sendNetworkMetric(mContext, params.str().c_str()); + InspectorHTTPServer::singleton().sendNetworkMetric(mContext, params.c_str()); #endif dumpNetworkMetricData(net, this->getUrl()); From d78c7041adeaaf8d013edd5847579206c5254457 Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 11:10:05 +0000 Subject: [PATCH 5/6] test --- src/InspectorHTTPServer.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/InspectorHTTPServer.cpp b/src/InspectorHTTPServer.cpp index e358a78..6a2b9f6 100644 --- a/src/InspectorHTTPServer.cpp +++ b/src/InspectorHTTPServer.cpp @@ -269,12 +269,9 @@ void InspectorHTTPServer::registerScript(const char* url, const char* source) << "\"executionContextId\":1,\"hash\":\"\"" << "}}"; std::string evtStr = evt.str(); - + std::lock_guard connectionsLock(m_connectionsMutex); for (const auto& pair : m_connections) { - SoupWebsocketConnection* conn = pair.first; - if (soup_websocket_connection_get_state(conn) == SOUP_WEBSOCKET_STATE_OPEN) { - soup_websocket_connection_send_text(conn, evtStr.c_str()); - } + sendText(pair.first, evtStr); } } From 7c9d9a92c989f4b932ec0e1dde2ef18daf69a39f Mon Sep 17 00:00:00 2001 From: gurpreet319 Date: Wed, 23 Sep 2026 11:35:16 +0000 Subject: [PATCH 6/6] test --- include/InspectorHTTPServer.h | 6 +- src/InspectorHTTPServer.cpp | 131 +++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 57 deletions(-) diff --git a/include/InspectorHTTPServer.h b/include/InspectorHTTPServer.h index 6bcd69a..20f5391 100644 --- a/include/InspectorHTTPServer.h +++ b/include/InspectorHTTPServer.h @@ -44,6 +44,7 @@ class InspectorHTTPServer { void sendConsoleMessage(JSContextRef context, const char* level, const char* text); + // Streams a single network metric to the connected frontend (no server-side storage). void sendNetworkMetric(JSContextRef context, const char* paramsJson); void registerScript(const char* url, const char* source); @@ -71,6 +72,8 @@ class InspectorHTTPServer { std::string generateTargetListJSON(); void handleCDPMessage(SoupWebsocketConnection* connection, const char* message); + bool sendText(SoupWebsocketConnection* connection, const std::string& message); + SoupWebsocketConnection* findConnectionForContext(JSGlobalContextRef context); struct ContextInfo { JSGlobalContextRef context; @@ -89,6 +92,8 @@ class InspectorHTTPServer { int m_port; std::map m_contexts; std::map m_connections; + std::mutex m_connectionsMutex; + std::mutex m_sendMutex; std::map m_scripts; std::mutex m_scriptsMutex; uint64_t m_nextContextId; @@ -97,4 +102,3 @@ class InspectorHTTPServer { }; #endif // REMOTE_INSPECTOR_ENABLE - diff --git a/src/InspectorHTTPServer.cpp b/src/InspectorHTTPServer.cpp index 6a2b9f6..2302719 100644 --- a/src/InspectorHTTPServer.cpp +++ b/src/InspectorHTTPServer.cpp @@ -91,6 +91,32 @@ InspectorHTTPServer::~InspectorHTTPServer() stop(); } +bool InspectorHTTPServer::sendText(SoupWebsocketConnection* connection, const std::string& message) +{ + if (!connection) { + return false; + } + + std::lock_guard sendLock(m_sendMutex); + if (soup_websocket_connection_get_state(connection) != SOUP_WEBSOCKET_STATE_OPEN) { + return false; + } + + soup_websocket_connection_send_text(connection, message.c_str()); + return true; +} + +SoupWebsocketConnection* InspectorHTTPServer::findConnectionForContext(JSGlobalContextRef context) +{ + std::lock_guard connectionsLock(m_connectionsMutex); + for (const auto& pair : m_connections) { + if (pair.second == context) { + return pair.first; + } + } + return nullptr; +} + bool InspectorHTTPServer::start(const char* address, int port) { if (m_server) { @@ -154,11 +180,12 @@ void InspectorHTTPServer::stop() m_server = nullptr; NativeJSLogger::log(INFO, "InspectorHTTPServer: Stopped\n"); } - + + std::lock_guard connectionsLock(m_connectionsMutex); for (auto& pair : m_connections) { g_object_unref(pair.first); } - + m_contexts.clear(); m_connections.clear(); } @@ -200,25 +227,13 @@ void InspectorHTTPServer::sendConsoleMessage(JSContextRef context, const char* l JSGlobalContextRef globalContext = const_cast(context); - SoupWebsocketConnection* targetConnection = nullptr; - for (const auto& pair : m_connections) { - if (pair.second == globalContext) { - targetConnection = pair.first; - break; - } - } - + SoupWebsocketConnection* targetConnection = findConnectionForContext(globalContext); + if (!targetConnection) { NativeJSLogger::log(DEBUG, "InspectorHTTPServer: No WebSocket connection found for this context\n"); return; } - - SoupWebsocketState state = soup_websocket_connection_get_state(targetConnection); - if (state != SOUP_WEBSOCKET_STATE_OPEN) { - NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connection is not open (state=%d)\n", state); - return; - } - + const std::string escapedText = escapeJSON(text); std::ostringstream event; @@ -230,8 +245,25 @@ void InspectorHTTPServer::sendConsoleMessage(JSContextRef context, const char* l << "}}"; std::string eventStr = event.str(); - soup_websocket_connection_send_text(targetConnection, eventStr.c_str()); - + if (!sendText(targetConnection, eventStr)) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connection is not open for console event\n"); + } +} + +void InspectorHTTPServer::sendNetworkMetric(JSContextRef context, const char* paramsJson) +{ + if (!context || !paramsJson) return; + + JSGlobalContextRef globalContext = const_cast(context); + + SoupWebsocketConnection* targetConnection = findConnectionForContext(globalContext); + + if (!targetConnection) return; + + std::ostringstream event; + event << "{\"method\":\"JSRuntime.networkMetric\",\"params\":" << paramsJson << "}"; + std::string eventStr = event.str(); + sendText(targetConnection, eventStr); } void InspectorHTTPServer::setReloadCallback(std::function callback) @@ -269,6 +301,7 @@ void InspectorHTTPServer::registerScript(const char* url, const char* source) << "\"executionContextId\":1,\"hash\":\"\"" << "}}"; std::string evtStr = evt.str(); + std::lock_guard connectionsLock(m_connectionsMutex); for (const auto& pair : m_connections) { sendText(pair.first, evtStr); @@ -413,7 +446,10 @@ void InspectorHTTPServer::onWebSocketRequest(SoupServer* server, SoupServerMessa if (targetContext) { g_object_ref(connection); - self->m_connections[connection] = targetContext; + { + std::lock_guard connectionsLock(self->m_connectionsMutex); + self->m_connections[connection] = targetContext; + } g_signal_connect(connection, "message", G_CALLBACK(onWebSocketMessage), userData); g_signal_connect(connection, "closed", G_CALLBACK(onWebSocketClosed), userData); @@ -452,7 +488,8 @@ void InspectorHTTPServer::onWebSocketMessage(SoupWebsocketConnection* connection void InspectorHTTPServer::onWebSocketClosed(SoupWebsocketConnection* connection, gpointer userData) { InspectorHTTPServer* self = static_cast(userData); - + + std::lock_guard connectionsLock(self->m_connectionsMutex); auto it = self->m_connections.find(connection); if (it != self->m_connections.end()) { NativeJSLogger::log(DEBUG, "InspectorHTTPServer: WebSocket connection closed\n"); @@ -463,14 +500,17 @@ void InspectorHTTPServer::onWebSocketClosed(SoupWebsocketConnection* connection, void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, const char* message) { - auto it = m_connections.find(connection); - if (it == m_connections.end()) { - NativeJSLogger::log(DEBUG, "InspectorHTTPServer: No context for connection\n"); - return; + JSGlobalContextRef context = nullptr; + { + std::lock_guard connectionsLock(m_connectionsMutex); + auto it = m_connections.find(connection); + if (it == m_connections.end()) { + NativeJSLogger::log(DEBUG, "InspectorHTTPServer: No context for connection\n"); + return; + } + context = it->second; } - - JSGlobalContextRef context = it->second; - + std::string msg(message); size_t idPos = msg.find("\"id\":"); @@ -502,7 +542,7 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, << "\"name\":\"RDK NativeScript\"" << "}}}"; std::string eventStr = contextCreated.str(); - soup_websocket_connection_send_text(connection, eventStr.c_str()); + sendText(connection, eventStr); } else if (method == "Debugger.enable" || method == "Console.enable" || method == "Runtime.runIfWaitingForDebugger" || @@ -528,7 +568,7 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, << "\"executionContextId\":1,\"hash\":\"\"" << "}}"; std::string evtStr = scriptParsed.str(); - soup_websocket_connection_send_text(connection, evtStr.c_str()); + sendText(connection, evtStr); } } } @@ -621,12 +661,12 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, // Ack first so frontend does not time out. response << "{\"id\":" << messageId << ",\"result\":{}}"; std::string ackStr = response.str(); - soup_websocket_connection_send_text(connection, ackStr.c_str()); + sendText(connection, ackStr); const char* destroyedEvt = "{\"method\":\"Runtime.executionContextDestroyed\"," "\"params\":{\"executionContextId\":1}}"; - soup_websocket_connection_send_text(connection, destroyedEvt); + sendText(connection, destroyedEvt); { std::lock_guard lock(m_scriptsMutex); @@ -644,7 +684,7 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, const char* createdEvt = "{\"method\":\"Runtime.executionContextCreated\",\"params\":{\"context\":{" "\"id\":1,\"origin\":\"\",\"name\":\"RDK NativeScript\"}}}"; - soup_websocket_connection_send_text(connection, createdEvt); + sendText(connection, createdEvt); NativeJSLogger::log(INFO, "InspectorHTTPServer: Page.reload complete\n"); return; @@ -731,30 +771,9 @@ void InspectorHTTPServer::handleCDPMessage(SoupWebsocketConnection* connection, } std::string responseStr = response.str(); - soup_websocket_connection_send_text(connection, responseStr.c_str()); + sendText(connection, responseStr); } -void InspectorHTTPServer::sendNetworkMetric(JSContextRef context, const char* paramsJson) -{ - if (!context || !paramsJson) return; - - JSGlobalContextRef globalContext = const_cast(context); - - SoupWebsocketConnection* targetConnection = nullptr; - for (const auto& pair : m_connections) { - if (pair.second == globalContext) { - targetConnection = pair.first; - break; - } - } - - if (!targetConnection) return; - if (soup_websocket_connection_get_state(targetConnection) != SOUP_WEBSOCKET_STATE_OPEN) return; - - std::ostringstream event; - event << "{\"method\":\"JSRuntime.networkMetric\",\"params\":" << paramsJson << "}"; - std::string eventStr = event.str(); - soup_websocket_connection_send_text(targetConnection, eventStr.c_str()); -} #endif // REMOTE_INSPECTOR_ENABLE +