assafvayner's picture
assafvayner HF Staff
save
a8d61e7
raw
history blame
11.9 kB
<script>
import { onMount } from "svelte";
let fileInput;
let backtraceData = "";
let threads = [];
let filteredThreads = [];
let filterText = "hf-xet-";
let visibleThreads = new Set();
// Parse the backtrace file format
function parseBacktraceFile(content) {
const lines = content.split("\n");
const parsedThreads = [];
let currentThread = null;
let currentBacktrace = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Simpler approach: look for lines containing "Thread_" with numbers at the start
if (line.includes("Thread_") && /^\s*\d+\s+Thread_\d+/.test(line)) {
// Save previous thread if exists
if (currentThread) {
currentThread.backtrace = currentBacktrace;
currentThread.collapsibleBacktrace =
createCollapsibleBacktrace(currentBacktrace);
parsedThreads.push(currentThread);
}
// Extract sample count and thread ID
const sampleMatch = line.match(/^\s*(\d+)\s+(Thread_\d+)/);
const sampleCount = sampleMatch[1];
const threadId = sampleMatch[2];
// Extract thread name (everything after the first colon, before parentheses)
let threadName = "";
const colonIndex = line.indexOf(":");
if (colonIndex !== -1) {
const afterColon = line.substring(colonIndex + 1);
const parenIndex = afterColon.indexOf("(");
threadName = (
parenIndex !== -1 ? afterColon.substring(0, parenIndex) : afterColon
).trim();
}
currentThread = {
id: threadId,
name: threadName,
fullName: threadName || threadId,
sampleCount: parseInt(sampleCount),
rawHeader: line.trim(),
backtrace: [],
expanded: false,
collapsibleBacktrace: null, // Will be created after backtrace is complete
};
currentBacktrace = [];
} else if (currentThread && line.trim()) {
// This is part of the backtrace
currentBacktrace.push(line);
}
// Skip empty lines
}
// Don't forget the last thread
if (currentThread) {
currentThread.backtrace = currentBacktrace;
currentThread.collapsibleBacktrace =
createCollapsibleBacktrace(currentBacktrace);
parsedThreads.push(currentThread);
}
return parsedThreads;
}
// Toggle thread visibility
function toggleThread(threadId) {
console.log("πŸ”„ Toggling thread:", threadId);
// Create a new array with updated thread to trigger Svelte reactivity
threads = threads.map((thread) => {
if (thread.id === threadId) {
const newThread = { ...thread, expanded: !thread.expanded };
console.log(
"πŸ“Š Thread expanded:",
newThread.expanded,
"backtrace lines:",
newThread.backtrace.length
);
return newThread;
}
return thread;
});
}
// Handle file upload
function handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
backtraceData = e.target.result;
threads = parseBacktraceFile(backtraceData);
};
reader.readAsText(file);
}
}
// Load sample file on mount
onMount(async () => {
try {
const response = await fetch("/sample.txt");
if (response.ok) {
backtraceData = await response.text();
threads = parseBacktraceFile(backtraceData);
console.log("βœ… Loaded", threads.length, "threads successfully");
}
} catch (error) {
console.error("Could not load sample.txt:", error);
}
});
// Parse individual backtrace lines for better display
function parseBacktraceLine(line) {
// Extract indentation
const indentMatch = line.match(/^(\s*[\+\!\:\|]*\s*)/);
const indent = indentMatch ? indentMatch[1] : "";
const content = line.substring(indent.length);
// Try to extract sample count, function name, and address
const parts = {
indent,
content,
original: line,
};
// Extract sample count if at beginning
const sampleMatch = content.match(/^(\d+)\s+(.+)/);
if (sampleMatch) {
parts.sampleCount = sampleMatch[1];
parts.content = sampleMatch[2];
}
return parts;
}
// Parse indentation level from a backtrace line
function getIndentationLevel(line) {
const match = line.match(/^(\s*[\+\!\:\|]*\s*)/);
return match ? match[1].length : 0;
}
// Find the end of a section (lines with greater indentation)
function findSectionEnd(backtrace, startIndex) {
const startIndentation = getIndentationLevel(backtrace[startIndex]);
for (let i = startIndex + 1; i < backtrace.length; i++) {
const currentIndentation = getIndentationLevel(backtrace[i]);
// If we find a line with same or lesser indentation, that's where the section ends
if (currentIndentation <= startIndentation) {
return i - 1; // Return the last line of the section
}
}
// If we reach the end, the section goes to the last line
return backtrace.length - 1;
}
// Create collapsible backtrace structure with section awareness
function createCollapsibleBacktrace(backtrace) {
const result = backtrace.map((line, index) => {
const indentationLevel = getIndentationLevel(line);
const sectionEnd = findSectionEnd(backtrace, index);
const hasSection = sectionEnd > index; // This line has child lines with greater indentation
return {
id: index,
line,
parsed: parseBacktraceLine(line),
indentationLevel,
hasSection,
sectionEnd,
sectionCollapsed: false,
hidden: false, // Will be set based on parent section state
};
});
// Initialize visibility based on any collapsed sections
result.forEach((_, index) => {
result[index].hidden = isLineHiddenByParentSection(result, index);
});
return result;
}
// Toggle section collapse for a specific line
function toggleSection(threadId, lineIndex) {
console.log(
"πŸ”„ Toggling section for thread:",
threadId,
"line:",
lineIndex
);
threads = threads.map((thread) => {
if (thread.id === threadId && thread.collapsibleBacktrace) {
const updatedBacktrace = [...thread.collapsibleBacktrace];
const line = updatedBacktrace[lineIndex];
if (line.hasSection) {
// Toggle the section collapsed state
line.sectionCollapsed = !line.sectionCollapsed;
console.log(
"πŸ“Š Section",
lineIndex,
"collapsed:",
line.sectionCollapsed,
"affects lines",
lineIndex + 1,
"to",
line.sectionEnd
);
// Update hidden state for all lines in this section
updateSectionVisibility(updatedBacktrace, lineIndex);
} else {
console.log("⚠️ Line", lineIndex, "has no section to toggle");
}
return { ...thread, collapsibleBacktrace: updatedBacktrace };
}
return thread;
});
}
// Update visibility of lines based on section collapse states
function updateSectionVisibility(backtrace, changedLineIndex) {
const changedLine = backtrace[changedLineIndex];
// Update visibility for lines in the changed section
for (let i = changedLineIndex + 1; i <= changedLine.sectionEnd; i++) {
if (changedLine.sectionCollapsed) {
backtrace[i].hidden = true;
} else {
// Only show if not hidden by a parent section
backtrace[i].hidden = isLineHiddenByParentSection(backtrace, i);
}
}
}
// Check if a line should be hidden by any parent section
function isLineHiddenByParentSection(backtrace, lineIndex) {
const currentIndentation = backtrace[lineIndex].indentationLevel;
// Look backwards for any parent sections that are collapsed
for (let i = lineIndex - 1; i >= 0; i--) {
const parentLine = backtrace[i];
if (
parentLine.indentationLevel < currentIndentation &&
parentLine.hasSection &&
parentLine.sectionCollapsed &&
i + 1 <= lineIndex &&
lineIndex <= parentLine.sectionEnd
) {
return true;
}
}
return false;
}
// Reactive updates - run filter when threads OR filterText changes
$: filteredThreads = threads.filter(
(thread) =>
!filterText.trim() ||
thread.fullName.toLowerCase().startsWith(filterText.toLowerCase())
);
</script>
<div class="header">
<h1>πŸ” Backtrace Viewer</h1>
<div class="controls">
<label for="fileInput" class="file-label"> πŸ“ Load Backtrace File </label>
<input
type="file"
id="fileInput"
class="file-input"
accept=".txt"
on:change={handleFileUpload}
bind:this={fileInput}
/>
<input
type="text"
class="filter-input"
placeholder="Filter threads by name prefix..."
bind:value={filterText}
/>
<div class="stats">
{filteredThreads.length} / {threads.length} threads
</div>
</div>
</div>
{#if threads.length === 0}
<div class="no-threads">
<p>No backtrace data loaded. Upload a file to get started.</p>
</div>
{:else}
{#each filteredThreads as thread (thread.id)}
<div class="thread-container">
<div
class="thread-header"
on:click={() => toggleThread(thread.id)}
on:keydown={(e) => e.key === "Enter" && toggleThread(thread.id)}
role="button"
tabindex="0"
>
<div class="thread-title">
<strong>{thread.sampleCount}</strong>
{thread.id}
{#if thread.name}
: <span style="color: #3498db;">{thread.name}</span>
{/if}
</div>
<div class="toggle-indicator" class:expanded={thread.expanded}>β–Ά</div>
</div>
{#if thread.expanded}
<div class="thread-content">
<div class="backtrace">
{#each thread.collapsibleBacktrace as backtraceLine (backtraceLine.id)}
{#if !backtraceLine.hidden}
{#if backtraceLine.hasSection}
<div
class="backtrace-line has-section"
class:section-collapsed={backtraceLine.sectionCollapsed}
on:click={() => toggleSection(thread.id, backtraceLine.id)}
on:keydown={(e) =>
e.key === "Enter" &&
toggleSection(thread.id, backtraceLine.id)}
role="button"
tabindex="0"
>
<span class="indent">{backtraceLine.parsed.indent}</span>
<span class="collapse-indicator">
{backtraceLine.sectionCollapsed ? "β–Ά" : "β–Ό"}
</span>
{#if backtraceLine.parsed.sampleCount}
<span class="sample-count"
>{backtraceLine.parsed.sampleCount}</span
>
{/if}
<span class="content">{backtraceLine.parsed.content}</span>
</div>
{:else}
<div class="backtrace-line">
<span class="indent">{backtraceLine.parsed.indent}</span>
{#if backtraceLine.parsed.sampleCount}
<span class="sample-count"
>{backtraceLine.parsed.sampleCount}</span
>
{/if}
<span class="content">{backtraceLine.parsed.content}</span>
</div>
{/if}
{/if}
{/each}
</div>
</div>
{/if}
</div>
{/each}
{/if}