assafvayner's picture
assafvayner HF Staff
move
c8bed06
<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) {
// Create a new array with updated thread to trigger Svelte reactivity
threads = threads.map((thread) => {
if (thread.id === threadId) {
return { ...thread, expanded: !thread.expanded };
}
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("samples/sample.txt");
if (response.ok) {
backtraceData = await response.text();
threads = parseBacktraceFile(backtraceData);
}
} 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;
}
// Check if a line starts with a numerical digit (0-9) after any leading + : | characters
function startsWithDigit(line) {
const trimmedLine = line.trim();
const result = /^[\+\!\:\|\s]*[0-9]/.test(trimmedLine);
if (
trimmedLine.includes("5898 tokio::runtime::scheduler::multi_thread::")
) {
console.log(trimmedLine, "\n\n\n", result);
}
return result;
}
// Find the end of a section for lines starting with digits
function findSectionEnd(backtrace, startIndex) {
const startIndentation = getIndentationLevel(backtrace[startIndex]);
for (let i = startIndex + 1; i < backtrace.length; i++) {
const currentLine = backtrace[i];
const currentIndentation = getIndentationLevel(currentLine);
// If we find a line that starts with a digit at same or lower indentation,
// that's where the section ends
if (
startsWithDigit(currentLine) &&
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 startsWithDigitFlag = startsWithDigit(line);
const sectionEnd = startsWithDigitFlag
? findSectionEnd(backtrace, index)
: index;
const hasSection = startsWithDigitFlag && sectionEnd > index; // Only lines starting with digits can have sections
return {
id: index,
line,
parsed: parseBacktraceLine(line),
indentationLevel,
hasSection,
sectionEnd,
sectionCollapsed: false, // Initially uncollapsed as requested
hidden: false, // Will be set based on parent section state
startsWithDigit: startsWithDigitFlag,
};
});
// 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) {
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;
// Update hidden state for all lines in this section
updateSectionVisibility(updatedBacktrace, lineIndex);
}
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 (must start with digit)
for (let i = lineIndex - 1; i >= 0; i--) {
const parentLine = backtrace[i];
if (
parentLine.startsWithDigit &&
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.startsWithDigit && 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 if backtraceLine.startsWithDigit && !backtraceLine.hasSection}
<!-- Lines starting with digits but no section (e.g., single line entries) -->
<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>
{:else}
<!-- Lines not starting with digits (continuation lines) -->
<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}