|
|
<script> |
|
|
import { onMount } from "svelte"; |
|
|
|
|
|
let fileInput; |
|
|
let backtraceData = ""; |
|
|
let threads = []; |
|
|
let filteredThreads = []; |
|
|
let filterText = "hf-xet-"; |
|
|
let visibleThreads = new Set(); |
|
|
|
|
|
|
|
|
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]; |
|
|
|
|
|
|
|
|
if (line.includes("Thread_") && /^\s*\d+\s+Thread_\d+/.test(line)) { |
|
|
|
|
|
if (currentThread) { |
|
|
currentThread.backtrace = currentBacktrace; |
|
|
currentThread.collapsibleBacktrace = |
|
|
createCollapsibleBacktrace(currentBacktrace); |
|
|
parsedThreads.push(currentThread); |
|
|
} |
|
|
|
|
|
|
|
|
const sampleMatch = line.match(/^\s*(\d+)\s+(Thread_\d+)/); |
|
|
const sampleCount = sampleMatch[1]; |
|
|
const threadId = sampleMatch[2]; |
|
|
|
|
|
|
|
|
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, |
|
|
}; |
|
|
currentBacktrace = []; |
|
|
} else if (currentThread && line.trim()) { |
|
|
|
|
|
currentBacktrace.push(line); |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (currentThread) { |
|
|
currentThread.backtrace = currentBacktrace; |
|
|
currentThread.collapsibleBacktrace = |
|
|
createCollapsibleBacktrace(currentBacktrace); |
|
|
parsedThreads.push(currentThread); |
|
|
} |
|
|
|
|
|
return parsedThreads; |
|
|
} |
|
|
|
|
|
|
|
|
function toggleThread(threadId) { |
|
|
|
|
|
threads = threads.map((thread) => { |
|
|
if (thread.id === threadId) { |
|
|
return { ...thread, expanded: !thread.expanded }; |
|
|
} |
|
|
return thread; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function parseBacktraceLine(line) { |
|
|
|
|
|
const indentMatch = line.match(/^(\s*[\+\!\:\|]*\s*)/); |
|
|
const indent = indentMatch ? indentMatch[1] : ""; |
|
|
const content = line.substring(indent.length); |
|
|
|
|
|
|
|
|
const parts = { |
|
|
indent, |
|
|
content, |
|
|
original: line, |
|
|
}; |
|
|
|
|
|
|
|
|
const sampleMatch = content.match(/^(\d+)\s+(.+)/); |
|
|
if (sampleMatch) { |
|
|
parts.sampleCount = sampleMatch[1]; |
|
|
parts.content = sampleMatch[2]; |
|
|
} |
|
|
|
|
|
return parts; |
|
|
} |
|
|
|
|
|
|
|
|
function getIndentationLevel(line) { |
|
|
const match = line.match(/^(\s*[\+\!\:\|]*\s*)/); |
|
|
return match ? match[1].length : 0; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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 ( |
|
|
startsWithDigit(currentLine) && |
|
|
currentIndentation <= startIndentation |
|
|
) { |
|
|
return i - 1; |
|
|
} |
|
|
} |
|
|
|
|
|
return backtrace.length - 1; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
return { |
|
|
id: index, |
|
|
line, |
|
|
parsed: parseBacktraceLine(line), |
|
|
indentationLevel, |
|
|
hasSection, |
|
|
sectionEnd, |
|
|
sectionCollapsed: false, |
|
|
hidden: false, |
|
|
startsWithDigit: startsWithDigitFlag, |
|
|
}; |
|
|
}); |
|
|
|
|
|
|
|
|
result.forEach((_, index) => { |
|
|
result[index].hidden = isLineHiddenByParentSection(result, index); |
|
|
}); |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
line.sectionCollapsed = !line.sectionCollapsed; |
|
|
|
|
|
updateSectionVisibility(updatedBacktrace, lineIndex); |
|
|
} |
|
|
|
|
|
return { ...thread, collapsibleBacktrace: updatedBacktrace }; |
|
|
} |
|
|
return thread; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateSectionVisibility(backtrace, changedLineIndex) { |
|
|
const changedLine = backtrace[changedLineIndex]; |
|
|
|
|
|
|
|
|
for (let i = changedLineIndex + 1; i <= changedLine.sectionEnd; i++) { |
|
|
if (changedLine.sectionCollapsed) { |
|
|
backtrace[i].hidden = true; |
|
|
} else { |
|
|
|
|
|
backtrace[i].hidden = isLineHiddenByParentSection(backtrace, i); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function isLineHiddenByParentSection(backtrace, lineIndex) { |
|
|
const currentIndentation = backtrace[lineIndex].indentationLevel; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
$: 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} |
|
|
|
|
|
<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} |
|
|
|
|
|
<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} |
|
|
|