codeflash-internal/js/cf-webapp/src/lib/lineProfilerParser.ts
Kevin Turcios e5374c3f50
fix: provide JWT_SECRET to CI build workflows (#2607)
## Summary
- Reverts lazy JWT_SECRET initialization — keeps eager fail-fast at
module load
- Adds `JWT_SECRET` secret to both `deploy_cfwebapp_to_azure.yml` and
`nextjs-build.yaml` CI workflows so `next build` page data collection
succeeds for the `/codeflash/auth/oauth/token` route

## Context
The deploy workflow ([run
#24425211765](https://github.com/codeflash-ai/codeflash-internal/actions/runs/24425211765/job/71357530269))
was failing because `JWT_SECRET` isn't available during CI build,
causing an eager throw at module load time. The secret already exists as
a GitHub repo secret.
2026-04-14 19:25:41 -05:00

345 lines
9.8 KiB
TypeScript

/**
* Line Profiler Parser Utility
* Parses the markdown table output from Python's line_profiler into structured data
*/
export interface LineProfilerEntry {
hits: string
time: string
perHit: string
percentTime: number
lineContents: string
}
export interface LineProfilerFunction {
functionName: string
totalTime: string
entries: LineProfilerEntry[]
}
export interface LineProfilerReport {
timerUnit: string
functions: LineProfilerFunction[]
}
/**
* Parse line profiler results from the markdown table format
* Expected format:
* ```
* # Timer unit: 1e-09 s
* ## Function: function_name
* ## Total time: X s
* | Hits | Time | Per Hit | % Time | Line Contents |
* |------|------|---------|--------|---------------|
* | 123 | 456 | 3.7 | 50.0 | def foo(): |
* ```
*
* TODO: Security & Robustness - Add input validation before processing:
* - Maximum input size limit (e.g., 10MB) to prevent DoS
* - Maximum line count limit (e.g., 100,000 lines)
* - Line length validation
* - Structure validation (expected headers, format)
*
* Example:
* ```typescript
* if (rawResults.length > 10_000_000) {
* throw new Error('Profiler output too large');
* }
* const lines = rawResults.split("\n");
* if (lines.length > 100_000) {
* throw new Error('Too many lines in profiler output');
* }
* ```
*/
export function parseLineProfilerResults(rawResults: string): LineProfilerReport {
const report: LineProfilerReport = {
timerUnit: "",
functions: [],
}
if (!rawResults || rawResults.trim() === "") {
return report
}
// Detect format: JS format starts with "Line Profile Results:" or has space-separated columns
const isJsFormat =
rawResults.includes("Line Profile Results:") ||
(rawResults.includes("Line") &&
rawResults.includes("Hits") &&
rawResults.includes("Content") &&
!rawResults.includes("|"))
if (isJsFormat) {
return parseJsLineProfilerResults(rawResults)
}
return parsePythonLineProfilerResults(rawResults)
}
/**
* Parse JS/TS line profiler format:
* Line Profile Results:
* File: /path/to/file.js
* --------------------------------------------------------------------------------
* Line Hits Time (ms) % Time Content
* --------------------------------------------------------------------------------
* 12 3442092 397.958 100.0% if (n <= 1) {
*/
function parseJsLineProfilerResults(rawResults: string): LineProfilerReport {
const report: LineProfilerReport = {
timerUnit: "1e-03 s", // JS profiler uses milliseconds
functions: [],
}
const lines = rawResults.split("\n")
let currentFunction: LineProfilerFunction | null = null
let inData = false
for (const line of lines) {
const trimmedLine = line.trim()
// Extract file path as function name
if (trimmedLine.startsWith("File:")) {
if (currentFunction) {
report.functions.push(currentFunction)
}
const filePath = trimmedLine.replace("File:", "").trim()
const fileName = filePath.split("/").pop() || filePath
currentFunction = {
functionName: fileName,
totalTime: "",
entries: [],
}
inData = false
continue
}
// Skip header line and separator lines
if (trimmedLine.startsWith("Line") && trimmedLine.includes("Hits")) {
inData = false
continue
}
if (trimmedLine.match(/^-+$/)) {
inData = true
continue
}
// Parse data rows: " 12 3442092 397.958 100.0% if (n <= 1) {"
if (inData && currentFunction && trimmedLine.length > 0) {
// Match: line_number, hits, time, percent, content
const match = trimmedLine.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\d.]+)%\s+(.*)$/)
if (match) {
const [, , hits, time, percent, content] = match
currentFunction.entries.push({
hits: hits,
time: time,
perHit: hits === "0" ? "0.000" : (parseFloat(time) / parseInt(hits)).toFixed(3),
percentTime: parseFloat(percent) || 0,
lineContents: content || " ",
})
}
}
}
if (currentFunction) {
report.functions.push(currentFunction)
}
return report
}
/**
* Parse Python line_profiler markdown table format
*/
function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport {
const report: LineProfilerReport = {
timerUnit: "",
functions: [],
}
const lines = rawResults.split("\n")
let currentFunction: LineProfilerFunction | null = null
let inTable = false
let headerPassed = false
for (const line of lines) {
const trimmedLine = line.trim()
// Parse timer unit
if (trimmedLine.startsWith("# Timer unit:")) {
report.timerUnit = trimmedLine.replace("# Timer unit:", "").trim()
continue
}
// Parse function name
if (trimmedLine.startsWith("## Function:")) {
if (currentFunction) {
report.functions.push(currentFunction)
}
currentFunction = {
functionName: trimmedLine.replace("## Function:", "").trim(),
totalTime: "",
entries: [],
}
inTable = false
headerPassed = false
continue
}
// Parse total time
if (trimmedLine.startsWith("## Total time:")) {
if (currentFunction) {
currentFunction.totalTime = trimmedLine.replace("## Total time:", "").trim()
}
continue
}
// Detect table header - handle variable whitespace in cells
// Tabulate may produce "| Hits |" with extra spaces
if (
trimmedLine.includes("Hits") &&
trimmedLine.includes("Line Contents") &&
trimmedLine.startsWith("|")
) {
inTable = true
headerPassed = false
continue
}
// Skip separator line (|---|---|...) - also handles alignment colons like |-------:|
if (inTable && trimmedLine.match(/^\|[-\s|:]+\|$/)) {
headerPassed = true
continue
}
// Parse table rows
if (inTable && headerPassed && trimmedLine.startsWith("|") && currentFunction) {
// Split by | but preserve the original line for extracting code with whitespace
const rawParts = trimmedLine.split("|")
// Trim stats columns but NOT the code column
const statParts = rawParts.slice(1, 5).map(p => p.trim())
// Keep code with original whitespace - join remaining parts (code may contain pipes)
// Use " " for empty lines to preserve blank lines in display
const codePart = rawParts.slice(5, -1).join("|") || " "
if (statParts.length >= 4) {
const percentStr = statParts[3] || "0"
const percentTime = parseFloat(percentStr) || 0
currentFunction.entries.push({
hits: statParts[0] || "",
time: statParts[1] || "",
perHit: statParts[2] || "",
percentTime,
lineContents: codePart, // Preserve whitespace in code
})
}
}
}
// Don't forget the last function
if (currentFunction) {
report.functions.push(currentFunction)
}
return report
}
/**
* Heat level thresholds for performance visualization (% of total time)
*/
export const HEAT_THRESHOLDS = {
HOT_4: 50, // >= 50% - Critical hotspot
HOT_3: 30, // >= 30% - High impact
HOT_2: 15, // >= 15% - Medium impact
HOT_1: 5, // >= 5% - Low impact
} as const
/**
* Get the heat level for a given percent time
* Returns a class suffix for CSS styling
*/
export function getHeatLevel(percentTime: number): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
if (percentTime >= HEAT_THRESHOLDS.HOT_4) return "hot-4"
if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3"
if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2"
if (percentTime >= HEAT_THRESHOLDS.HOT_1) return "hot-1"
return "cold"
}
/**
* Calculate the maximum percent time across all entries for normalization
*/
export function getMaxPercentTime(report: LineProfilerReport): number {
let max = 0
for (const func of report.functions) {
for (const entry of func.entries) {
if (entry.percentTime > max) {
max = entry.percentTime
}
}
}
return max || 100
}
/**
* Format timer unit to human-readable string
* e.g., "1e-09 s" → "1 ns", "1e-06 s" → "1 µs"
*/
export function formatTimerUnit(timerUnit: string): string {
if (!timerUnit || timerUnit.trim() === "") return ""
const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/)
if (!unitMatch) return timerUnit
const scaleFactor = parseFloat(unitMatch[1])
if (scaleFactor >= 1) {
return `${scaleFactor} s`
} else if (scaleFactor >= 1e-3) {
return `${scaleFactor * 1000} ms`
} else if (scaleFactor >= 1e-6) {
return `${scaleFactor * 1e6} µs`
} else if (scaleFactor >= 1e-9) {
return `${scaleFactor * 1e9} ns`
} else {
return `${scaleFactor * 1e12} ps`
}
}
/**
* Format time value to human-readable string
*/
export function formatTime(timeStr: string, timerUnit: string): string {
if (!timeStr || timeStr === "") return ""
// Parse the timer unit to get the scale factor
// Common units: "1e-09 s" (nanoseconds), "1e-06 s" (microseconds), etc.
let scaleFactor = 1e-9 // Default to nanoseconds
const unitMatch = timerUnit.match(/([0-9.e+-]+)\s*s/)
if (unitMatch) {
scaleFactor = parseFloat(unitMatch[1])
}
// Parse the time value
const timeValue = parseFloat(timeStr.replace(/,/g, ""))
if (isNaN(timeValue)) return timeStr
// Convert to seconds
const seconds = timeValue * scaleFactor
// Format based on magnitude
if (seconds >= 3600) {
return `${(seconds / 3600).toFixed(2)}h`
} else if (seconds >= 60) {
return `${(seconds / 60).toFixed(2)}m`
} else if (seconds >= 1) {
return `${seconds.toFixed(2)}s`
} else if (seconds >= 0.001) {
return `${(seconds * 1000).toFixed(2)}ms`
} else if (seconds >= 0.000001) {
return `${(seconds * 1000000).toFixed(2)}µs`
} else {
return `${(seconds * 1000000000).toFixed(2)}ns`
}
}