Attribute SFI ops in trace map; collapse long file lists
This commit is contained in:
1 parent
8cb6c4d547
commit
ef1ba00732
1 file changed
+50
-16
+50
-16
@@ -149,6 +149,8 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
|
|||||||
.side-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
.side-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||||
.map-item { padding: 3px 8px; cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--text); word-break: break-word; }
|
.map-item { padding: 3px 8px; cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--text); word-break: break-word; }
|
||||||
.map-item:hover { background: var(--bg-input); }
|
.map-item:hover { background: var(--bg-input); }
|
||||||
|
.map-files-toggle { color: var(--accent); cursor: pointer; }
|
||||||
|
.map-files-toggle .map-arrow { color: var(--text-muted); }
|
||||||
|
|
||||||
/* Status bar */
|
/* Status bar */
|
||||||
.status-bar { display: flex; padding: 8px 16px; background: var(--bg-panel); border-top: 1px solid var(--bg-input); font-size: 13px; gap: 16px; flex-shrink: 0; }
|
.status-bar { display: flex; padding: 8px 16px; background: var(--bg-panel); border-top: 1px solid var(--bg-input); font-size: 13px; gap: 16px; flex-shrink: 0; }
|
||||||
@@ -359,6 +361,7 @@ const STATE = {
|
|||||||
serverVersion: '',
|
serverVersion: '',
|
||||||
captureMode: '',
|
captureMode: '',
|
||||||
deviceConnected: null,
|
deviceConnected: null,
|
||||||
|
mapExpanded: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_BASE = () => STATE.serverUrl;
|
const API_BASE = () => STATE.serverUrl;
|
||||||
@@ -1037,6 +1040,8 @@ function buildTraceMap() {
|
|||||||
if (ins === 'c0') { i++; continue; } // skip GET RESPONSE
|
if (ins === 'c0') { i++; continue; } // skip GET RESPONSE
|
||||||
if (ins && MAP_RW_INS.has(ins)) {
|
if (ins && MAP_RW_INS.has(ins)) {
|
||||||
// Split the run into per-file records (delimited by SELECT).
|
// Split the run into per-file records (delimited by SELECT).
|
||||||
|
// Each READ/UPDATE op contributes its own resolved file, so
|
||||||
|
// SFI-addressed files (never SELECTed) are attributed correctly.
|
||||||
const access = [];
|
const access = [];
|
||||||
let cur = null;
|
let cur = null;
|
||||||
let j = i;
|
let j = i;
|
||||||
@@ -1046,47 +1051,50 @@ function buildTraceMap() {
|
|||||||
if (!jins || !MAP_RW_INS.has(jins)) break;
|
if (!jins || !MAP_RW_INS.has(jins)) break;
|
||||||
if (jins === 'a4') {
|
if (jins === 'a4') {
|
||||||
if (cur) access.push(cur);
|
if (cur) access.push(cur);
|
||||||
cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j };
|
cur = { files: [], selNote: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j };
|
||||||
} else if (is_write_ins(jins)) {
|
} else {
|
||||||
if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j };
|
if (!cur) cur = { files: [], selNote: null, hasRead: false, hasWrite: false, start: j, end: j };
|
||||||
cur.hasWrite = true;
|
const f = map_file_label(msgs[j]);
|
||||||
cur.end = j;
|
if (f && !cur.files.includes(f)) cur.files.push(f);
|
||||||
} else { // SEARCH / READ
|
if (is_write_ins(jins)) cur.hasWrite = true; else cur.hasRead = true;
|
||||||
if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j };
|
|
||||||
cur.hasRead = true;
|
|
||||||
cur.end = j;
|
cur.end = j;
|
||||||
}
|
}
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
if (cur) access.push(cur);
|
if (cur) access.push(cur);
|
||||||
|
// Records with no read/write ops (pure catalog SELECT) fall back to
|
||||||
|
// the selected file's note.
|
||||||
|
for (const rec of access) {
|
||||||
|
if (rec.files.length === 0 && rec.selNote) rec.files = [rec.selNote];
|
||||||
|
}
|
||||||
|
|
||||||
let k = 0;
|
let k = 0;
|
||||||
while (k < access.length) {
|
while (k < access.length) {
|
||||||
const rec = access[k];
|
const rec = access[k];
|
||||||
if (rec.hasRead && rec.hasWrite) {
|
if (rec.hasRead && rec.hasWrite) {
|
||||||
groups.push({ label: t('Read/write:') + ' ' + (rec.file || '—'), start: rec.start, end: rec.end });
|
groups.push({ kind: 'rw', files: rec.files, start: rec.start, end: rec.end });
|
||||||
k++;
|
k++;
|
||||||
} else if (rec.hasWrite) {
|
} else if (rec.hasWrite) {
|
||||||
const files = [];
|
const files = [];
|
||||||
let end = rec.end;
|
let end = rec.end;
|
||||||
let m = k;
|
let m = k;
|
||||||
while (m < access.length && access[m].hasWrite && !access[m].hasRead) {
|
while (m < access.length && access[m].hasWrite && !access[m].hasRead) {
|
||||||
if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file);
|
for (const f of access[m].files) if (f && !files.includes(f)) files.push(f);
|
||||||
end = access[m].end;
|
end = access[m].end;
|
||||||
m++;
|
m++;
|
||||||
}
|
}
|
||||||
groups.push({ label: t('Writes:') + ' ' + (files.join(', ') || '—'), start: rec.start, end });
|
groups.push({ kind: 'write', files, start: rec.start, end });
|
||||||
k = m;
|
k = m;
|
||||||
} else {
|
} else {
|
||||||
const files = [];
|
const files = [];
|
||||||
let end = rec.end;
|
let end = rec.end;
|
||||||
let m = k;
|
let m = k;
|
||||||
while (m < access.length && !access[m].hasWrite) {
|
while (m < access.length && !access[m].hasWrite) {
|
||||||
if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file);
|
for (const f of access[m].files) if (f && !files.includes(f)) files.push(f);
|
||||||
end = access[m].end;
|
end = access[m].end;
|
||||||
m++;
|
m++;
|
||||||
}
|
}
|
||||||
groups.push({ label: t('Reads:') + ' ' + (files.join(', ') || '—'), start: rec.start, end });
|
groups.push({ kind: 'read', files, start: rec.start, end });
|
||||||
k = m;
|
k = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1113,9 +1121,13 @@ function renderSidePanel() {
|
|||||||
).join('') || t('No commands');
|
).join('') || t('No commands');
|
||||||
|
|
||||||
const mapEl = el('type-map');
|
const mapEl = el('type-map');
|
||||||
mapEl.innerHTML = STATE.traceMap.map(g =>
|
mapEl.innerHTML = STATE.traceMap.map(g => {
|
||||||
`<div class="map-item" data-start="${g.start}">${g.label}</div>`
|
if (g.kind) {
|
||||||
).join('') || t('No commands');
|
const kind = g.kind === 'rw' ? t('Read/write:') : g.kind === 'write' ? t('Writes:') : t('Reads:');
|
||||||
|
return `<div class="map-item" data-start="${g.start}">${kind} ${mapFilesHtml(g)}</div>`;
|
||||||
|
}
|
||||||
|
return `<div class="map-item" data-start="${g.start}">${g.label}</div>`;
|
||||||
|
}).join('') || t('No commands');
|
||||||
|
|
||||||
const showMap = STATE.sideView === 'map';
|
const showMap = STATE.sideView === 'map';
|
||||||
el('type-counts').style.display = showMap ? 'none' : '';
|
el('type-counts').style.display = showMap ? 'none' : '';
|
||||||
@@ -1125,6 +1137,15 @@ function renderSidePanel() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapFilesHtml(g) {
|
||||||
|
const files = g.files || [];
|
||||||
|
if (files.length === 0) return '—';
|
||||||
|
if (files.length <= 3) return files.join(', ');
|
||||||
|
const expanded = STATE.mapExpanded.has(g.start);
|
||||||
|
const label = expanded ? files.join(', ') : t('{n} files').replace('{n}', files.length);
|
||||||
|
return `<span class="map-files-toggle" data-start="${g.start}">${label} <span class="map-arrow">${expanded ? '▾' : '▸'}</span></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function populateFilterOptions() {
|
function populateFilterOptions() {
|
||||||
const sel = el('filter-select');
|
const sel = el('filter-select');
|
||||||
const counts = {};
|
const counts = {};
|
||||||
@@ -1358,6 +1379,7 @@ async function startCapture() {
|
|||||||
STATE.currentSessionId = data.session_id;
|
STATE.currentSessionId = data.session_id;
|
||||||
STATE.messages = [];
|
STATE.messages = [];
|
||||||
_lastAfter = 0;
|
_lastAfter = 0;
|
||||||
|
STATE.mapExpanded.clear();
|
||||||
buildTypeCounts();
|
buildTypeCounts();
|
||||||
buildTraceMap();
|
buildTraceMap();
|
||||||
renderSidePanel();
|
renderSidePanel();
|
||||||
@@ -1508,6 +1530,7 @@ async function loadSession(id) {
|
|||||||
STATE.captureActive = false;
|
STATE.captureActive = false;
|
||||||
STATE.currentSessionName = data.session.name || t('Session #') + id;
|
STATE.currentSessionName = data.session.name || t('Session #') + id;
|
||||||
STATE.messages = data.messages;
|
STATE.messages = data.messages;
|
||||||
|
STATE.mapExpanded.clear();
|
||||||
switchTab('trace');
|
switchTab('trace');
|
||||||
updateCaptureButton();
|
updateCaptureButton();
|
||||||
renderMessages();
|
renderMessages();
|
||||||
@@ -1684,6 +1707,7 @@ async function checkConnection() {
|
|||||||
STATE.currentSessionId = data.session_id;
|
STATE.currentSessionId = data.session_id;
|
||||||
STATE.messages = [];
|
STATE.messages = [];
|
||||||
_lastAfter = 0;
|
_lastAfter = 0;
|
||||||
|
STATE.mapExpanded.clear();
|
||||||
}
|
}
|
||||||
STATE.captureActive = true;
|
STATE.captureActive = true;
|
||||||
STATE.currentSessionId = data.session_id;
|
STATE.currentSessionId = data.session_id;
|
||||||
@@ -1800,6 +1824,7 @@ const LANG_RU = {
|
|||||||
'Reads:': 'Чтение:',
|
'Reads:': 'Чтение:',
|
||||||
'Writes:': 'Запись:',
|
'Writes:': 'Запись:',
|
||||||
'Read/write:': 'Чтение/запись:',
|
'Read/write:': 'Чтение/запись:',
|
||||||
|
'{n} files': '{n} файлов',
|
||||||
'Not connected': 'Не подключено',
|
'Not connected': 'Не подключено',
|
||||||
'Live': 'Захват',
|
'Live': 'Захват',
|
||||||
'Session': 'Сессия',
|
'Session': 'Сессия',
|
||||||
@@ -2057,6 +2082,15 @@ function init() {
|
|||||||
renderSidePanel();
|
renderSidePanel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const toggle = e.target.closest('.map-files-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const start = parseInt(toggle.dataset.start);
|
||||||
|
if (STATE.mapExpanded.has(start)) STATE.mapExpanded.delete(start);
|
||||||
|
else STATE.mapExpanded.add(start);
|
||||||
|
renderSidePanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const mapItem = e.target.closest('.map-item');
|
const mapItem = e.target.closest('.map-item');
|
||||||
if (mapItem) {
|
if (mapItem) {
|
||||||
focusMessage(parseInt(mapItem.dataset.start));
|
focusMessage(parseInt(mapItem.dataset.start));
|
||||||
|
|||||||
Reference in new issue
Block a user