Add JavaScript viewer and HTTP request initiator tracking
Introduces comprehensive request initiator visualization and JavaScript performance analysis capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8075e54397
commit
488d9a2650
@ -73,4 +73,4 @@ This is a React + TypeScript + Vite project called "perfviz" - a modern web appl
|
||||
- **React Integration**: Proper cleanup with engine disposal in useEffect return function
|
||||
- **Version-Specific Documentation**: ALWAYS check Babylon.js documentation for version 8.21.1 specifically to avoid deprecated methods and ensure current API usage
|
||||
- **API Verification**: Before suggesting any Babylon.js code, verify method signatures and availability in the current version
|
||||
-
|
||||
- **CSS**: Use CSS modules for styling components, avoid inline styles for better performance and maintainability
|
244498
examples/chek-plp.json
Normal file
244498
examples/chek-plp.json
Normal file
File diff suppressed because one or more lines are too long
20
src/App.tsx
20
src/App.tsx
@ -3,13 +3,14 @@ import styles from './App.module.css'
|
||||
import TraceViewer from './components/TraceViewer'
|
||||
import PhaseViewer from './components/PhaseViewer'
|
||||
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
|
||||
import JavaScriptViewer from './components/javascriptviewer/JavaScriptViewer'
|
||||
import RequestDebugger from './components/RequestDebugger'
|
||||
import TraceUpload from './components/TraceUpload'
|
||||
import TraceSelector from './components/TraceSelector'
|
||||
import { traceDatabase } from './utils/traceDatabase'
|
||||
import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
|
||||
|
||||
type AppView = 'trace' | 'phases' | 'http' | 'debug'
|
||||
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'debug'
|
||||
type AppMode = 'selector' | 'upload' | 'analysis'
|
||||
type ThreeDView = 'network' | 'timeline' | null
|
||||
|
||||
@ -33,7 +34,7 @@ const getUrlParams = () => {
|
||||
const view = segments[1] as AppView
|
||||
const threeDView = segments[2] as ThreeDView
|
||||
// Validate view and 3D view values
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
||||
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
|
||||
const validatedView = validViews.includes(view) ? view : 'http'
|
||||
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
|
||||
@ -42,7 +43,7 @@ const getUrlParams = () => {
|
||||
const traceId = segments[0]
|
||||
const view = segments[1] as AppView
|
||||
// Validate view is one of the allowed values
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
||||
const validatedView = validViews.includes(view) ? view : 'http'
|
||||
return { traceId, view: validatedView, threeDView: null }
|
||||
} else if (segments.length === 1) {
|
||||
@ -219,6 +220,15 @@ function App() {
|
||||
>
|
||||
HTTP Requests
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.navButton} ${currentView === 'js' ? styles.active : ''}`}
|
||||
onClick={() => {
|
||||
setCurrentView('js')
|
||||
updateUrlWithTraceId(selectedTraceId, 'js', null)
|
||||
}}
|
||||
>
|
||||
JavaScript
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.navButton} ${currentView === 'debug' ? styles.active : ''}`}
|
||||
onClick={() => {
|
||||
@ -243,6 +253,10 @@ function App() {
|
||||
<HTTPRequestViewer traceId={selectedTraceId} />
|
||||
)}
|
||||
|
||||
{currentView === 'js' && (
|
||||
<JavaScriptViewer traceId={selectedTraceId} />
|
||||
)}
|
||||
|
||||
{currentView === 'debug' && traceData && (
|
||||
<RequestDebugger traceEvents={traceData.traceEvents} />
|
||||
)}
|
||||
|
211
src/components/httprequestviewer/InitiatorChain.module.css
Normal file
211
src/components/httprequestviewer/InitiatorChain.module.css
Normal file
@ -0,0 +1,211 @@
|
||||
/* Initiator Chain styles using CSS variables from App.module.css */
|
||||
|
||||
.chainContainer {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.chainHeader {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.chainTitle {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.chainDescription {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.noChain {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-bg-light);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Chain tree structure */
|
||||
.chainTree {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chainNodeContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chainNode {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chainNode.selected .nodeContent {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(var(--color-primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.chainNode.selected .nodeUrl,
|
||||
.chainNode.selected .nodeInfo {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chainNode.selected .resourceType,
|
||||
.chainNode.selected .initiatorType {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* Connection lines */
|
||||
.nodeConnector {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.verticalLine {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: -12px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 12px;
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* Node content */
|
||||
.nodeContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-sm);
|
||||
flex: 1;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nodeContent:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
font-size: var(--font-size-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nodeDetails {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.nodeUrl {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nodeInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.resourceType {
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.initiatorType {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.selectedIndicator {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.chainContainer {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.chainNode {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.nodeConnector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nodeContent {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.nodeInfo {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.selectedIndicator {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chainHeader {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.nodeDetails {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nodeUrl {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
189
src/components/httprequestviewer/InitiatorChain.tsx
Normal file
189
src/components/httprequestviewer/InitiatorChain.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { HTTPRequest } from './types/httpRequest'
|
||||
import styles from './InitiatorChain.module.css'
|
||||
|
||||
interface InitiatorChainProps {
|
||||
requests: HTTPRequest[]
|
||||
selectedRequest: HTTPRequest
|
||||
}
|
||||
|
||||
interface ChainNode {
|
||||
request: HTTPRequest
|
||||
level: number
|
||||
children: ChainNode[]
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
const InitiatorChain: React.FC<InitiatorChainProps> = ({ requests, selectedRequest }) => {
|
||||
// Build the initiator chain tree
|
||||
const chainTree = useMemo(() => {
|
||||
const buildChain = (request: HTTPRequest, level: number = 0, visited: Set<string> = new Set()): ChainNode => {
|
||||
// Prevent infinite loops
|
||||
if (visited.has(request.requestId)) {
|
||||
return {
|
||||
request,
|
||||
level,
|
||||
children: [],
|
||||
isSelected: request.requestId === selectedRequest.requestId
|
||||
}
|
||||
}
|
||||
|
||||
visited.add(request.requestId)
|
||||
|
||||
// Find requests initiated by this request
|
||||
const children = requests
|
||||
.filter(r => {
|
||||
if (!r.initiator || r.requestId === request.requestId) return false
|
||||
|
||||
// Check if this request was initiated by the current request
|
||||
if (r.initiator.type === 'script' && r.initiator.stack) {
|
||||
// Look for the current request's URL in the call stack
|
||||
return r.initiator.stack.callFrames.some(frame =>
|
||||
frame.url === request.url ||
|
||||
frame.url.includes(new URL(request.url).pathname)
|
||||
)
|
||||
}
|
||||
|
||||
if (r.initiator.type === 'parser' && r.initiator.url) {
|
||||
return r.initiator.url === request.url
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
.map(r => buildChain(r, level + 1, new Set(visited)))
|
||||
|
||||
return {
|
||||
request,
|
||||
level,
|
||||
children,
|
||||
isSelected: request.requestId === selectedRequest.requestId
|
||||
}
|
||||
}
|
||||
|
||||
// Find the root requests (those without initiators or with external initiators)
|
||||
const rootRequests = requests.filter(r => {
|
||||
if (!r.initiator) return true
|
||||
if (r.initiator.type === 'parser' && r.initiator.url) {
|
||||
// Check if the initiator URL is in our request list
|
||||
return !requests.some(req => req.url === r.initiator!.url)
|
||||
}
|
||||
return r.initiator.type === 'other' || r.initiator.type === 'preload'
|
||||
})
|
||||
|
||||
// Build chains for each root
|
||||
const chains = rootRequests.map(root => buildChain(root))
|
||||
|
||||
// If the selected request is not in any chain, add it as a standalone chain
|
||||
const selectedInChains = chains.some(chain =>
|
||||
findNodeInChain(chain, selectedRequest.requestId)
|
||||
)
|
||||
|
||||
if (!selectedInChains) {
|
||||
chains.push(buildChain(selectedRequest))
|
||||
}
|
||||
|
||||
return chains
|
||||
}, [requests, selectedRequest])
|
||||
|
||||
const findNodeInChain = (node: ChainNode, requestId: string): boolean => {
|
||||
if (node.request.requestId === requestId) return true
|
||||
return node.children.some(child => findNodeInChain(child, requestId))
|
||||
}
|
||||
|
||||
const getResourceTypeIcon = (resourceType: string) => {
|
||||
switch (resourceType.toLowerCase()) {
|
||||
case 'document': return '📄'
|
||||
case 'script': return '📜'
|
||||
case 'stylesheet': return '🎨'
|
||||
case 'image': return '🖼️'
|
||||
case 'font': return '🔤'
|
||||
case 'xhr': return '📡'
|
||||
case 'fetch': return '📤'
|
||||
default: return '📋'
|
||||
}
|
||||
}
|
||||
|
||||
const truncateUrl = (url: string, maxLength: number = 50) => {
|
||||
if (url.length <= maxLength) return url
|
||||
const start = url.substring(0, 20)
|
||||
const end = url.substring(url.length - 25)
|
||||
return `${start}...${end}`
|
||||
}
|
||||
|
||||
const renderChainNode = (node: ChainNode) => {
|
||||
const { request, level, children, isSelected } = node
|
||||
const indentation = level * 20
|
||||
|
||||
return (
|
||||
<div key={request.requestId} className={styles.chainNodeContainer}>
|
||||
<div
|
||||
className={`${styles.chainNode} ${isSelected ? styles.selected : ''}`}
|
||||
style={{ marginLeft: `${indentation}px` }}
|
||||
>
|
||||
<div className={styles.nodeConnector}>
|
||||
{level > 0 && (
|
||||
<>
|
||||
<div className={styles.verticalLine} />
|
||||
<div className={styles.horizontalLine} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.nodeContent}>
|
||||
<div className={styles.nodeIcon}>
|
||||
{getResourceTypeIcon(request.resourceType)}
|
||||
</div>
|
||||
|
||||
<div className={styles.nodeDetails}>
|
||||
<div className={styles.nodeUrl} title={request.url}>
|
||||
{truncateUrl(request.url)}
|
||||
</div>
|
||||
<div className={styles.nodeInfo}>
|
||||
<span className={styles.resourceType}>{request.resourceType}</span>
|
||||
{request.initiator && (
|
||||
<span className={styles.initiatorType}>
|
||||
via {request.initiator.type}
|
||||
{request.initiator.fetchType && ` (${request.initiator.fetchType})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSelected && (
|
||||
<div className={styles.selectedIndicator}>
|
||||
📍 Selected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children.map(child => renderChainNode(child))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (chainTree.length === 0) {
|
||||
return (
|
||||
<div className={styles.noChain}>
|
||||
No initiator chain information available
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.chainContainer}>
|
||||
<div className={styles.chainHeader}>
|
||||
<h4 className={styles.chainTitle}>🔗 Request Initiator Chain</h4>
|
||||
<div className={styles.chainDescription}>
|
||||
Shows how requests are initiated by other requests or by the browser
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.chainTree}>
|
||||
{chainTree.map(rootNode => renderChainNode(rootNode))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InitiatorChain
|
256
src/components/httprequestviewer/InitiatorView.module.css
Normal file
256
src/components/httprequestviewer/InitiatorView.module.css
Normal file
@ -0,0 +1,256 @@
|
||||
/* Initiator View styles using CSS variables from App.module.css */
|
||||
|
||||
.initiatorContainer {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.initiatorHeader {
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* No initiator state */
|
||||
.noInitiator {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-bg-light);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Initiator basic information */
|
||||
.initiatorInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.infoRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
min-width: 80px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Section titles */
|
||||
.sectionTitle {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Parser information */
|
||||
.parserInfo {
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.parserDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.parserUrl {
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.parserLocation {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
/* Call stack information */
|
||||
.stackInfo {
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.callStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.callFrame {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.callFrame:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.frameIndex {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.frameDetails {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.frameFunction {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-mono);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.frameLocation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.frameUrl {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.frameUrl:hover {
|
||||
color: var(--color-text-highlight);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.framePosition {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-secondary);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-xs);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Basic info for other types */
|
||||
.basicInfo {
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.basicInfoText {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
/* URL links */
|
||||
.urlLink {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.urlLink:hover {
|
||||
color: var(--color-text-highlight);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.initiatorContainer {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.infoRow {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.frameLocation {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.frameUrl {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.callFrame {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.frameIndex {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
158
src/components/httprequestviewer/InitiatorView.tsx
Normal file
158
src/components/httprequestviewer/InitiatorView.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React from 'react'
|
||||
import type { HTTPRequest, CallFrame } from './types/httpRequest'
|
||||
import styles from './InitiatorView.module.css'
|
||||
|
||||
interface InitiatorViewProps {
|
||||
request: HTTPRequest
|
||||
}
|
||||
|
||||
const InitiatorView: React.FC<InitiatorViewProps> = ({ request }) => {
|
||||
if (!request.initiator) {
|
||||
return (
|
||||
<div className={styles.noInitiator}>
|
||||
No initiator information available
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { initiator } = request
|
||||
const hasStack = initiator.stack && initiator.stack.callFrames.length > 0
|
||||
|
||||
const getInitiatorTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'parser': return '📄'
|
||||
case 'script': return '📜'
|
||||
case 'preload': return '⚡'
|
||||
default: return '❓'
|
||||
}
|
||||
}
|
||||
|
||||
const getFetchTypeIcon = (fetchType?: string) => {
|
||||
switch (fetchType) {
|
||||
case 'script': return '🔧'
|
||||
case 'link': return '🔗'
|
||||
case 'fetch': return '📡'
|
||||
case 'xhr': return '📤'
|
||||
case 'img': return '🖼️'
|
||||
default: return '📋'
|
||||
}
|
||||
}
|
||||
|
||||
const truncateUrl = (url: string, maxLength: number = 60) => {
|
||||
if (url.length <= maxLength) return url
|
||||
const start = url.substring(0, 20)
|
||||
const end = url.substring(url.length - 35)
|
||||
return `${start}...${end}`
|
||||
}
|
||||
|
||||
const formatLocation = (lineNumber?: number, columnNumber?: number) => {
|
||||
if (lineNumber !== undefined && columnNumber !== undefined) {
|
||||
return `${lineNumber}:${columnNumber}`
|
||||
}
|
||||
if (lineNumber !== undefined) {
|
||||
return `${lineNumber}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
const renderCallFrame = (frame: CallFrame, index: number) => (
|
||||
<div key={index} className={styles.callFrame}>
|
||||
<div className={styles.frameIndex}>{index + 1}</div>
|
||||
<div className={styles.frameDetails}>
|
||||
<div className={styles.frameFunction}>
|
||||
{frame.functionName || '(anonymous)'}
|
||||
</div>
|
||||
<div className={styles.frameLocation}>
|
||||
<a
|
||||
href={frame.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.frameUrl}
|
||||
title={frame.url}
|
||||
>
|
||||
{truncateUrl(frame.url)}
|
||||
</a>
|
||||
<span className={styles.framePosition}>
|
||||
@ {formatLocation(frame.lineNumber, frame.columnNumber)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.initiatorContainer}>
|
||||
<div className={styles.initiatorHeader}>
|
||||
<h4 className={styles.title}>Request Initiator</h4>
|
||||
</div>
|
||||
|
||||
{/* Initiator Type Information */}
|
||||
<div className={styles.initiatorInfo}>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>Type:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{getInitiatorTypeIcon(initiator.type)} {initiator.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{initiator.fetchType && (
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>Fetch Type:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{getFetchTypeIcon(initiator.fetchType)} {initiator.fetchType}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Parser-initiated requests */}
|
||||
{initiator.type === 'parser' && initiator.url && (
|
||||
<div className={styles.parserInfo}>
|
||||
<h5 className={styles.sectionTitle}>📄 Document Parser</h5>
|
||||
<div className={styles.parserDetails}>
|
||||
<div className={styles.parserUrl}>
|
||||
<a
|
||||
href={initiator.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.urlLink}
|
||||
title={initiator.url}
|
||||
>
|
||||
{truncateUrl(initiator.url)}
|
||||
</a>
|
||||
</div>
|
||||
{(initiator.lineNumber || initiator.columnNumber) && (
|
||||
<div className={styles.parserLocation}>
|
||||
Line {formatLocation(initiator.lineNumber, initiator.columnNumber)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Script-initiated requests with call stack */}
|
||||
{initiator.type === 'script' && hasStack && (
|
||||
<div className={styles.stackInfo}>
|
||||
<h5 className={styles.sectionTitle}>📜 Call Stack</h5>
|
||||
<div className={styles.callStack}>
|
||||
{initiator.stack!.callFrames.map((frame, index) =>
|
||||
renderCallFrame(frame, index)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other types without detailed information */}
|
||||
{!hasStack && initiator.type !== 'parser' && (
|
||||
<div className={styles.basicInfo}>
|
||||
<div className={styles.basicInfoText}>
|
||||
This request was initiated by {initiator.type}
|
||||
{initiator.fetchType && ` using ${initiator.fetchType}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InitiatorView
|
@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import type { HTTPRequest } from './types/httpRequest'
|
||||
import InitiatorView from './InitiatorView'
|
||||
import styles from './RequestRowDetails.module.css'
|
||||
|
||||
// Import utility functions
|
||||
@ -44,6 +45,9 @@ const RequestRowDetails: React.FC<RequestRowDetailsProps> = ({ request, visibleC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Initiator Information */}
|
||||
<InitiatorView request={request} />
|
||||
|
||||
{/* Network Timing */}
|
||||
<div className={styles.detailCard}>
|
||||
<h4 className={styles.detailCardTitle}>Network Timing</h4>
|
||||
|
@ -214,6 +214,7 @@ const RequestsTable: React.FC<RequestsTableProps> = ({
|
||||
<RequestRowSummary
|
||||
key={request.requestId}
|
||||
request={request}
|
||||
allRequests={httpRequests}
|
||||
showQueueAnalysis={showQueueAnalysis}
|
||||
isExpanded={isExpanded}
|
||||
onToggleRowExpansion={onToggleRowExpansion}
|
||||
|
@ -48,6 +48,19 @@ export const processHTTPRequests = (traceEvents: TraceEvent[]): HTTPRequest[] =>
|
||||
if (args.data.connectionId !== undefined) {
|
||||
request.connectionId = args.data.connectionId
|
||||
}
|
||||
|
||||
// Extract initiator information
|
||||
if (args.data.initiator) {
|
||||
const initiator = args.data.initiator
|
||||
request.initiator = {
|
||||
type: initiator.type || 'other',
|
||||
fetchType: initiator.fetchType,
|
||||
url: initiator.url,
|
||||
lineNumber: initiator.lineNumber,
|
||||
columnNumber: initiator.columnNumber,
|
||||
stack: initiator.stack
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'ResourceReceiveResponse':
|
||||
|
@ -23,6 +23,27 @@ export interface ScreenshotEvent {
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface CallFrame {
|
||||
scriptId: string
|
||||
url: string
|
||||
lineNumber: number
|
||||
columnNumber: number
|
||||
functionName: string
|
||||
}
|
||||
|
||||
export interface InitiatorStack {
|
||||
callFrames: CallFrame[]
|
||||
}
|
||||
|
||||
export interface RequestInitiator {
|
||||
type: 'parser' | 'script' | 'preload' | 'other'
|
||||
fetchType?: 'script' | 'link' | 'fetch' | 'xhr' | 'img' | 'other'
|
||||
url?: string
|
||||
lineNumber?: number
|
||||
columnNumber?: number
|
||||
stack?: InitiatorStack
|
||||
}
|
||||
|
||||
export interface HTTPRequest {
|
||||
requestId: string
|
||||
url: string
|
||||
@ -71,4 +92,5 @@ export interface HTTPRequest {
|
||||
connectionReused: boolean
|
||||
queueAnalysis?: QueueAnalysis
|
||||
cdnAnalysis?: CDNAnalysis
|
||||
initiator?: RequestInitiator
|
||||
}
|
184
src/components/javascriptviewer/JavaScriptEventsTable.module.css
Normal file
184
src/components/javascriptviewer/JavaScriptEventsTable.module.css
Normal file
@ -0,0 +1,184 @@
|
||||
/* JavaScript Events Table styles using CSS variables from App.module.css */
|
||||
|
||||
.tableContainer {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.tableHeaderCell {
|
||||
padding: var(--spacing-sm);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tableHeaderCell.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tableHeaderCell.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tableHeaderCell.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.eventRow {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.eventRow:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.eventRow td {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-base);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.eventRow td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Event icon column */
|
||||
.eventIcon {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-lg);
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.eventIcon.primary { color: var(--color-primary); }
|
||||
.eventIcon.secondary { color: var(--color-text-muted); }
|
||||
.eventIcon.success { color: var(--color-success); }
|
||||
.eventIcon.warning { color: var(--color-warning); }
|
||||
.eventIcon.danger { color: var(--color-danger); }
|
||||
.eventIcon.info { color: var(--color-primary); }
|
||||
.eventIcon.muted { color: var(--color-text-muted); }
|
||||
|
||||
/* Event type column */
|
||||
.eventType {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Duration column with performance indicators */
|
||||
.duration {
|
||||
text-align: right;
|
||||
font-family: var(--font-family-mono);
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.duration.success {
|
||||
color: var(--color-success);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.duration.warning {
|
||||
color: var(--color-warning);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.duration.danger {
|
||||
color: var(--color-danger);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* URL column */
|
||||
.url {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.urlLink {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.urlLink:hover {
|
||||
color: var(--color-text-highlight);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.inlineScript {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Location column */
|
||||
.location {
|
||||
text-align: center;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Script type column */
|
||||
.scriptType {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scriptType.primary { color: var(--color-primary); }
|
||||
.scriptType.secondary { color: var(--color-text-muted); }
|
||||
.scriptType.success { color: var(--color-success); }
|
||||
.scriptType.warning { color: var(--color-warning); }
|
||||
.scriptType.info { color: var(--color-primary); }
|
||||
.scriptType.muted { color: var(--color-text-muted); }
|
||||
|
||||
/* Streaming column */
|
||||
.streaming {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-lg);
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
/* Start time column */
|
||||
.startTime {
|
||||
text-align: right;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* No results message */
|
||||
.noResults {
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
padding: var(--spacing-lg);
|
||||
font-size: var(--font-size-xl);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
147
src/components/javascriptviewer/JavaScriptEventsTable.tsx
Normal file
147
src/components/javascriptviewer/JavaScriptEventsTable.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import React from 'react'
|
||||
import type { JavaScriptEvent } from './types/javascriptEvent'
|
||||
import {
|
||||
formatDuration,
|
||||
formatEventType,
|
||||
getEventIcon,
|
||||
truncateUrl,
|
||||
getScriptType,
|
||||
formatLocation
|
||||
} from './lib/formatUtils'
|
||||
import {
|
||||
getDurationClass,
|
||||
getEventTypeClass,
|
||||
getScriptTypeClass,
|
||||
getStreamingIndicator
|
||||
} from './lib/colorUtils'
|
||||
import styles from './JavaScriptEventsTable.module.css'
|
||||
|
||||
interface JavaScriptEventsTableProps {
|
||||
events: JavaScriptEvent[]
|
||||
visibleColumns: Record<string, boolean>
|
||||
}
|
||||
|
||||
const JavaScriptEventsTable: React.FC<JavaScriptEventsTableProps> = ({
|
||||
events,
|
||||
visibleColumns
|
||||
}) => {
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<div className={styles.noResults}>
|
||||
No JavaScript events found in the trace data
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.tableContainer}>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.tableHeader}>
|
||||
<tr>
|
||||
{visibleColumns.icon && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
|
||||
Type
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.eventType && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.left}`}>
|
||||
Event
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.duration && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
|
||||
Duration
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.url && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.left}`}>
|
||||
Script / URL
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.location && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
|
||||
Location
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.scriptType && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
|
||||
Script Type
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.streaming && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
|
||||
Streaming
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.startTime && (
|
||||
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
|
||||
Start Time
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map((event) => (
|
||||
<tr key={event.eventId} className={styles.eventRow}>
|
||||
{visibleColumns.icon && (
|
||||
<td className={`${styles.eventIcon} ${getEventTypeClass(event.name)}`}>
|
||||
{getEventIcon(event.name)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.eventType && (
|
||||
<td className={styles.eventType}>
|
||||
{formatEventType(event.name)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.duration && (
|
||||
<td className={`${styles.duration} ${getDurationClass(event.timing.durationMs)}`}>
|
||||
{formatDuration(event.timing.duration)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.url && (
|
||||
<td className={styles.url} title={event.url}>
|
||||
{event.isInlineScript ? (
|
||||
<span className={styles.inlineScript}>
|
||||
📄 Inline script
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={event.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.urlLink}
|
||||
>
|
||||
{truncateUrl(event.url || '')}
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.location && (
|
||||
<td className={styles.location}>
|
||||
{formatLocation(event.lineNumber, event.columnNumber)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.scriptType && (
|
||||
<td className={`${styles.scriptType} ${getScriptTypeClass(getScriptType(event))}`}>
|
||||
{getScriptType(event)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.streaming && (
|
||||
<td className={styles.streaming} title={event.v8Data?.notStreamedReason}>
|
||||
{getStreamingIndicator(event.v8Data?.streamed, event.v8Data?.notStreamedReason)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.startTime && (
|
||||
<td className={styles.startTime}>
|
||||
{formatDuration(event.timing.startTime)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JavaScriptEventsTable
|
182
src/components/javascriptviewer/JavaScriptSummary.module.css
Normal file
182
src/components/javascriptviewer/JavaScriptSummary.module.css
Normal file
@ -0,0 +1,182 @@
|
||||
/* JavaScript Summary styles using CSS variables from App.module.css */
|
||||
|
||||
.summaryContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Overview Stats Grid */
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.statCard:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-highlight);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Breakdown Section */
|
||||
.breakdownSection {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: bold;
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.eventTypeGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.eventTypeCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.eventTypeCard:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.eventTypeIcon {
|
||||
font-size: var(--font-size-xxl);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.eventTypeInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.eventTypeCount {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-highlight);
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.eventTypeLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Top Scripts Section */
|
||||
.topScriptsSection {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.scriptsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.scriptCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
background: var(--color-bg-light);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.scriptCard:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.scriptRank {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.scriptInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scriptUrl {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scriptStats {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scriptTime {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.scriptEvents {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
111
src/components/javascriptviewer/JavaScriptSummary.tsx
Normal file
111
src/components/javascriptviewer/JavaScriptSummary.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import type { JavaScriptSummary as JSummary } from './types/javascriptEvent'
|
||||
import { formatDuration } from './lib/formatUtils'
|
||||
import styles from './JavaScriptSummary.module.css'
|
||||
|
||||
interface JavaScriptSummaryProps {
|
||||
summary: JSummary
|
||||
}
|
||||
|
||||
const JavaScriptSummary: React.FC<JavaScriptSummaryProps> = ({ summary }) => {
|
||||
const {
|
||||
totalEvents,
|
||||
totalExecutionTime,
|
||||
totalCompilationTime,
|
||||
totalEvaluationTime,
|
||||
eventsByType,
|
||||
scriptsByUrl
|
||||
} = summary
|
||||
|
||||
const topScripts = Array.from(scriptsByUrl.values())
|
||||
.sort((a, b) => b.totalTime - a.totalTime)
|
||||
.slice(0, 5)
|
||||
|
||||
return (
|
||||
<div className={styles.summaryContainer}>
|
||||
{/* Overview Stats */}
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statValue}>{totalEvents}</div>
|
||||
<div className={styles.statLabel}>Total Events</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statValue}>{formatDuration(totalExecutionTime)}</div>
|
||||
<div className={styles.statLabel}>Total Time</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statValue}>{formatDuration(totalCompilationTime)}</div>
|
||||
<div className={styles.statLabel}>Compilation</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statValue}>{formatDuration(totalEvaluationTime)}</div>
|
||||
<div className={styles.statLabel}>Evaluation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Type Breakdown */}
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3 className={styles.sectionTitle}>Event Type Breakdown</h3>
|
||||
<div className={styles.eventTypeGrid}>
|
||||
<div className={styles.eventTypeCard}>
|
||||
<div className={styles.eventTypeIcon}>⚙️</div>
|
||||
<div className={styles.eventTypeInfo}>
|
||||
<div className={styles.eventTypeCount}>{eventsByType.compilation}</div>
|
||||
<div className={styles.eventTypeLabel}>Compilation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.eventTypeCard}>
|
||||
<div className={styles.eventTypeIcon}>▶️</div>
|
||||
<div className={styles.eventTypeInfo}>
|
||||
<div className={styles.eventTypeCount}>{eventsByType.evaluation}</div>
|
||||
<div className={styles.eventTypeLabel}>Evaluation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.eventTypeCard}>
|
||||
<div className={styles.eventTypeIcon}>📞</div>
|
||||
<div className={styles.eventTypeInfo}>
|
||||
<div className={styles.eventTypeCount}>{eventsByType.execution}</div>
|
||||
<div className={styles.eventTypeLabel}>Execution</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.eventTypeCard}>
|
||||
<div className={styles.eventTypeIcon}>🔧</div>
|
||||
<div className={styles.eventTypeInfo}>
|
||||
<div className={styles.eventTypeCount}>{eventsByType.other}</div>
|
||||
<div className={styles.eventTypeLabel}>Other</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Scripts by Time */}
|
||||
<div className={styles.topScriptsSection}>
|
||||
<h3 className={styles.sectionTitle}>Top Scripts by Execution Time</h3>
|
||||
<div className={styles.scriptsList}>
|
||||
{topScripts.map((script, index) => (
|
||||
<div key={script.url} className={styles.scriptCard}>
|
||||
<div className={styles.scriptRank}>#{index + 1}</div>
|
||||
<div className={styles.scriptInfo}>
|
||||
<div className={styles.scriptUrl} title={script.url}>
|
||||
{script.isThirdParty ? '🌐' : '🏠'} {script.url}
|
||||
</div>
|
||||
<div className={styles.scriptStats}>
|
||||
<span className={styles.scriptTime}>{formatDuration(script.totalTime)}</span>
|
||||
<span className={styles.scriptEvents}>{script.events.length} events</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JavaScriptSummary
|
155
src/components/javascriptviewer/JavaScriptViewer.module.css
Normal file
155
src/components/javascriptviewer/JavaScriptViewer.module.css
Normal file
@ -0,0 +1,155 @@
|
||||
/* JavaScript Viewer styles using CSS variables from App.module.css */
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
/* Loading and error states */
|
||||
.loading,
|
||||
.error,
|
||||
.noData {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
font-size: var(--font-size-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.loading {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.noData {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Filters section */
|
||||
.filtersContainer {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.filtersTitle {
|
||||
color: var(--color-text-highlight);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: bold;
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.filterSelect,
|
||||
.filterInput {
|
||||
padding: var(--spacing-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg-primary);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-base);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filterSelect:focus,
|
||||
.filterInput:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filterInput::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Results info */
|
||||
.resultsInfo {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
padding-top: var(--spacing-sm);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: var(--spacing-md);
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.filtersContainer {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
}
|
160
src/components/javascriptviewer/JavaScriptViewer.tsx
Normal file
160
src/components/javascriptviewer/JavaScriptViewer.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
|
||||
import { processJavaScriptEvents, calculateJavaScriptSummary } from './lib/javascriptProcessor'
|
||||
import JavaScriptSummary from './JavaScriptSummary'
|
||||
import JavaScriptEventsTable from './JavaScriptEventsTable'
|
||||
import styles from './JavaScriptViewer.module.css'
|
||||
|
||||
interface JavaScriptViewerProps {
|
||||
traceId: string | null
|
||||
}
|
||||
|
||||
const JavaScriptViewer: React.FC<JavaScriptViewerProps> = ({ traceId }) => {
|
||||
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
||||
|
||||
// Column visibility state for the table
|
||||
const [visibleColumns] = useState(() => ({
|
||||
icon: true,
|
||||
eventType: true,
|
||||
duration: true,
|
||||
url: true,
|
||||
location: true,
|
||||
scriptType: true,
|
||||
streaming: true,
|
||||
startTime: false
|
||||
}))
|
||||
|
||||
// Filter states
|
||||
const [eventTypeFilter, setEventTypeFilter] = useState<string>('all')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [minDuration, setMinDuration] = useState<number>(0)
|
||||
|
||||
const jsEvents = useMemo(() => {
|
||||
if (!traceData) return []
|
||||
return processJavaScriptEvents(traceData.traceEvents)
|
||||
}, [traceData])
|
||||
|
||||
const filteredEvents = useMemo(() => {
|
||||
let events = jsEvents
|
||||
|
||||
// Filter by event type
|
||||
if (eventTypeFilter !== 'all') {
|
||||
events = events.filter(event => event.name === eventTypeFilter)
|
||||
}
|
||||
|
||||
// Filter by minimum duration (in milliseconds)
|
||||
if (minDuration > 0) {
|
||||
events = events.filter(event => event.timing.durationMs >= minDuration)
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
events = events.filter(event =>
|
||||
event.url?.toLowerCase().includes(term) ||
|
||||
event.fileName?.toLowerCase().includes(term) ||
|
||||
event.name.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
|
||||
return events
|
||||
}, [jsEvents, eventTypeFilter, minDuration, searchTerm])
|
||||
|
||||
const summary = useMemo(() => {
|
||||
return calculateJavaScriptSummary(jsEvents)
|
||||
}, [jsEvents])
|
||||
|
||||
// Extract unique event types for filter dropdown
|
||||
const eventTypes = useMemo(() => {
|
||||
const types = Array.from(new Set(jsEvents.map(event => event.name)))
|
||||
return types.sort()
|
||||
}, [jsEvents])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
Loading JavaScript performance data...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
Error loading trace data: {error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!traceData) {
|
||||
return (
|
||||
<div className={styles.noData}>
|
||||
No trace data available
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.title}>JavaScript Performance Analysis</h2>
|
||||
|
||||
{/* Summary Statistics */}
|
||||
<JavaScriptSummary summary={summary} />
|
||||
|
||||
{/* Filters */}
|
||||
<div className={styles.filtersContainer}>
|
||||
<h3 className={styles.filtersTitle}>Filters</h3>
|
||||
<div className={styles.filters}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>Event Type:</label>
|
||||
<select
|
||||
value={eventTypeFilter}
|
||||
onChange={(e) => setEventTypeFilter(e.target.value)}
|
||||
className={styles.filterSelect}
|
||||
>
|
||||
<option value="all">All Events</option>
|
||||
{eventTypes.map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>Min Duration (ms):</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={minDuration}
|
||||
onChange={(e) => setMinDuration(parseFloat(e.target.value) || 0)}
|
||||
className={styles.filterInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>Search:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search URLs, files, or event types..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className={styles.filterInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.resultsInfo}>
|
||||
Showing {filteredEvents.length} of {jsEvents.length} JavaScript events
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Events Table */}
|
||||
<JavaScriptEventsTable
|
||||
events={filteredEvents}
|
||||
visibleColumns={visibleColumns}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JavaScriptViewer
|
64
src/components/javascriptviewer/lib/colorUtils.ts
Normal file
64
src/components/javascriptviewer/lib/colorUtils.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// Color utilities for JavaScript performance indicators
|
||||
|
||||
export const getDurationClass = (durationMs: number): string => {
|
||||
if (durationMs > 50) {
|
||||
return 'danger' // Red for > 50ms
|
||||
} else if (durationMs >= 10) {
|
||||
return 'warning' // Orange for 10-50ms
|
||||
} else {
|
||||
return 'success' // Green for < 10ms
|
||||
}
|
||||
}
|
||||
|
||||
export const getEventTypeClass = (eventName: string): string => {
|
||||
switch (eventName) {
|
||||
case 'EvaluateScript':
|
||||
return 'primary'
|
||||
case 'v8.compile':
|
||||
return 'secondary'
|
||||
case 'v8.callFunction':
|
||||
return 'info'
|
||||
case 'v8.newInstance':
|
||||
return 'success'
|
||||
case 'V8.DeoptimizeAllOptimizedCodeWithFunction':
|
||||
return 'danger'
|
||||
case 'CpuProfiler::StartProfiling':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'muted'
|
||||
}
|
||||
}
|
||||
|
||||
export const getScriptTypeClass = (scriptType: string): string => {
|
||||
switch (scriptType) {
|
||||
case 'Inline':
|
||||
return 'warning'
|
||||
case 'External':
|
||||
return 'success'
|
||||
case 'Bundle':
|
||||
return 'primary'
|
||||
case 'Minified':
|
||||
return 'info'
|
||||
case 'Extension':
|
||||
return 'muted'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
export const getThirdPartyIndicator = (isThirdParty: boolean): string => {
|
||||
return isThirdParty ? '🌐' : '🏠'
|
||||
}
|
||||
|
||||
export const getStreamingIndicator = (streamed?: boolean, reason?: string): string => {
|
||||
if (streamed === true) return '🚀'
|
||||
if (streamed === false && reason) {
|
||||
switch (reason) {
|
||||
case 'inline script':
|
||||
return '📄'
|
||||
default:
|
||||
return '⏳'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
97
src/components/javascriptviewer/lib/formatUtils.ts
Normal file
97
src/components/javascriptviewer/lib/formatUtils.ts
Normal file
@ -0,0 +1,97 @@
|
||||
// Utility functions for formatting JavaScript performance data
|
||||
|
||||
export const formatDuration = (microseconds?: number): string => {
|
||||
if (!microseconds || microseconds === 0) return '0ms'
|
||||
|
||||
const ms = microseconds / 1000
|
||||
|
||||
if (ms < 1) {
|
||||
return `${microseconds.toFixed(0)}μs`
|
||||
} else if (ms < 10) {
|
||||
return `${ms.toFixed(2)}ms`
|
||||
} else if (ms < 100) {
|
||||
return `${ms.toFixed(1)}ms`
|
||||
} else {
|
||||
return `${ms.toFixed(0)}ms`
|
||||
}
|
||||
}
|
||||
|
||||
export const formatEventType = (eventName: string): string => {
|
||||
switch (eventName) {
|
||||
case 'EvaluateScript':
|
||||
return 'Script Evaluation'
|
||||
case 'v8.compile':
|
||||
return 'Compilation'
|
||||
case 'v8.callFunction':
|
||||
return 'Function Call'
|
||||
case 'v8.newInstance':
|
||||
return 'Object Creation'
|
||||
case 'V8.DeoptimizeAllOptimizedCodeWithFunction':
|
||||
return 'Deoptimization'
|
||||
case 'CpuProfiler::StartProfiling':
|
||||
return 'Profiler Start'
|
||||
case 'ScriptCatchup':
|
||||
return 'Script Catchup'
|
||||
default:
|
||||
return eventName
|
||||
}
|
||||
}
|
||||
|
||||
export const getEventIcon = (eventName: string): string => {
|
||||
switch (eventName) {
|
||||
case 'EvaluateScript':
|
||||
return '▶️'
|
||||
case 'v8.compile':
|
||||
return '⚙️'
|
||||
case 'v8.callFunction':
|
||||
return '📞'
|
||||
case 'v8.newInstance':
|
||||
return '🆕'
|
||||
case 'V8.DeoptimizeAllOptimizedCodeWithFunction':
|
||||
return '⚠️'
|
||||
case 'CpuProfiler::StartProfiling':
|
||||
return '📊'
|
||||
case 'ScriptCatchup':
|
||||
return '🔄'
|
||||
default:
|
||||
return '🔧'
|
||||
}
|
||||
}
|
||||
|
||||
export const truncateUrl = (url: string, maxLength: number = 80): string => {
|
||||
if (!url || url.length <= maxLength) return url
|
||||
|
||||
// For file URLs, show the filename
|
||||
if (url.includes('/')) {
|
||||
const parts = url.split('/')
|
||||
const filename = parts[parts.length - 1]
|
||||
if (filename.length <= maxLength) {
|
||||
return `.../${filename}`
|
||||
}
|
||||
}
|
||||
|
||||
// General truncation
|
||||
return url.length > maxLength
|
||||
? `${url.substring(0, maxLength - 3)}...`
|
||||
: url
|
||||
}
|
||||
|
||||
export const getScriptType = (event: { url?: string, isInlineScript: boolean }): string => {
|
||||
if (event.isInlineScript) return 'Inline'
|
||||
if (!event.url) return 'Unknown'
|
||||
|
||||
if (event.url.includes('.min.js')) return 'Minified'
|
||||
if (event.url.includes('chunk') || event.url.includes('bundle')) return 'Bundle'
|
||||
if (event.url.startsWith('chrome-extension://')) return 'Extension'
|
||||
|
||||
return 'External'
|
||||
}
|
||||
|
||||
export const formatLocation = (lineNumber?: number, columnNumber?: number): string => {
|
||||
if (lineNumber !== undefined && columnNumber !== undefined) {
|
||||
return `${lineNumber}:${columnNumber}`
|
||||
} else if (lineNumber !== undefined) {
|
||||
return `Line ${lineNumber}`
|
||||
}
|
||||
return '-'
|
||||
}
|
156
src/components/javascriptviewer/lib/javascriptProcessor.ts
Normal file
156
src/components/javascriptviewer/lib/javascriptProcessor.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import type { TraceEvent } from '../../../types/trace'
|
||||
import type { JavaScriptEvent, JavaScriptSummary } from '../types/javascriptEvent'
|
||||
|
||||
// JavaScript event names we're interested in
|
||||
const JS_EVENT_NAMES = [
|
||||
'EvaluateScript',
|
||||
'v8.compile',
|
||||
'v8.callFunction',
|
||||
'v8.newInstance',
|
||||
'V8.DeoptimizeAllOptimizedCodeWithFunction',
|
||||
'CpuProfiler::StartProfiling',
|
||||
'ScriptCatchup'
|
||||
] as const
|
||||
|
||||
export const processJavaScriptEvents = (traceEvents: TraceEvent[]): JavaScriptEvent[] => {
|
||||
const jsEvents: JavaScriptEvent[] = []
|
||||
|
||||
for (const event of traceEvents) {
|
||||
// Filter for JavaScript-related events
|
||||
if (!JS_EVENT_NAMES.includes(event.name as any)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip events without timing data
|
||||
if (!event.dur && !event.ts) {
|
||||
continue
|
||||
}
|
||||
|
||||
const args = event.args as any
|
||||
const eventId = `${event.name}-${event.ts}-${event.tid}`
|
||||
|
||||
// Extract URL and script information
|
||||
let url = ''
|
||||
let fileName = ''
|
||||
let lineNumber: number | undefined
|
||||
let columnNumber: number | undefined
|
||||
let isInlineScript = false
|
||||
|
||||
if (args?.data?.url) {
|
||||
url = args.data.url
|
||||
fileName = args.fileName || url
|
||||
isInlineScript = args.data.notStreamedReason === 'inline script' || !url.startsWith('http')
|
||||
} else if (args?.fileName) {
|
||||
fileName = args.fileName
|
||||
url = fileName
|
||||
}
|
||||
|
||||
if (args?.data?.lineNumber !== undefined) {
|
||||
lineNumber = args.data.lineNumber
|
||||
}
|
||||
if (args?.data?.columnNumber !== undefined) {
|
||||
columnNumber = args.data.columnNumber
|
||||
}
|
||||
|
||||
const duration = event.dur || 0
|
||||
|
||||
const jsEvent: JavaScriptEvent = {
|
||||
eventId,
|
||||
name: event.name,
|
||||
category: event.cat || '',
|
||||
url,
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
isInlineScript,
|
||||
scriptId: args?.data?.scriptId?.toString(),
|
||||
sampleTraceId: args?.data?.sampleTraceId?.toString(),
|
||||
|
||||
timing: {
|
||||
startTime: event.ts,
|
||||
duration,
|
||||
threadDuration: event.tdur,
|
||||
durationMs: duration / 1000 // Convert to milliseconds
|
||||
},
|
||||
|
||||
v8Data: {
|
||||
streamed: args?.data?.streamed,
|
||||
notStreamedReason: args?.data?.notStreamedReason,
|
||||
isolate: args?.data?.isolate?.toString(),
|
||||
executionContextId: args?.data?.executionContextId
|
||||
},
|
||||
|
||||
rawEvent: event
|
||||
}
|
||||
|
||||
jsEvents.push(jsEvent)
|
||||
}
|
||||
|
||||
// Sort by start time
|
||||
return jsEvents.sort((a, b) => a.timing.startTime - b.timing.startTime)
|
||||
}
|
||||
|
||||
export const calculateJavaScriptSummary = (events: JavaScriptEvent[]): JavaScriptSummary => {
|
||||
let totalExecutionTime = 0
|
||||
let totalCompilationTime = 0
|
||||
let totalEvaluationTime = 0
|
||||
|
||||
const eventsByType = {
|
||||
compilation: 0,
|
||||
evaluation: 0,
|
||||
execution: 0,
|
||||
other: 0
|
||||
}
|
||||
|
||||
const scriptsByUrl = new Map<string, {
|
||||
url: string
|
||||
events: JavaScriptEvent[]
|
||||
totalTime: number
|
||||
isThirdParty: boolean
|
||||
}>()
|
||||
|
||||
for (const event of events) {
|
||||
// Categorize by event type
|
||||
if (event.name === 'v8.compile') {
|
||||
totalCompilationTime += event.timing.duration
|
||||
eventsByType.compilation++
|
||||
} else if (event.name === 'EvaluateScript') {
|
||||
totalEvaluationTime += event.timing.duration
|
||||
eventsByType.evaluation++
|
||||
} else if (event.name.includes('callFunction') || event.name.includes('newInstance')) {
|
||||
totalExecutionTime += event.timing.duration
|
||||
eventsByType.execution++
|
||||
} else {
|
||||
eventsByType.other++
|
||||
}
|
||||
|
||||
// Group by URL/script
|
||||
if (event.url) {
|
||||
const url = event.isInlineScript ? '(inline scripts)' : event.url
|
||||
const isThirdParty = event.url.startsWith('http') &&
|
||||
!event.url.includes(window.location?.hostname || 'localhost')
|
||||
|
||||
if (!scriptsByUrl.has(url)) {
|
||||
scriptsByUrl.set(url, {
|
||||
url,
|
||||
events: [],
|
||||
totalTime: 0,
|
||||
isThirdParty
|
||||
})
|
||||
}
|
||||
|
||||
const script = scriptsByUrl.get(url)!
|
||||
script.events.push(event)
|
||||
script.totalTime += event.timing.duration
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalEvents: events.length,
|
||||
totalExecutionTime: totalExecutionTime + totalCompilationTime + totalEvaluationTime,
|
||||
totalCompilationTime,
|
||||
totalEvaluationTime,
|
||||
eventsByType,
|
||||
scriptsByUrl
|
||||
}
|
||||
}
|
54
src/components/javascriptviewer/types/javascriptEvent.ts
Normal file
54
src/components/javascriptviewer/types/javascriptEvent.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type { TraceEvent } from '../../../types/trace'
|
||||
|
||||
export interface JavaScriptEvent {
|
||||
eventId: string
|
||||
name: string // EvaluateScript, v8.compile, v8.callFunction, etc.
|
||||
category: string
|
||||
url?: string
|
||||
fileName?: string
|
||||
lineNumber?: number
|
||||
columnNumber?: number
|
||||
isInlineScript: boolean
|
||||
scriptId?: string
|
||||
sampleTraceId?: string
|
||||
|
||||
// Timing information
|
||||
timing: {
|
||||
startTime: number // ts in microseconds
|
||||
duration: number // dur in microseconds
|
||||
threadDuration?: number // tdur in microseconds
|
||||
durationMs: number // duration in milliseconds for display
|
||||
}
|
||||
|
||||
// V8 specific data
|
||||
v8Data?: {
|
||||
streamed?: boolean
|
||||
notStreamedReason?: string
|
||||
isolate?: string
|
||||
executionContextId?: number
|
||||
}
|
||||
|
||||
// Raw trace event for debugging
|
||||
rawEvent: TraceEvent
|
||||
}
|
||||
|
||||
export interface JavaScriptSummary {
|
||||
totalEvents: number
|
||||
totalExecutionTime: number // in microseconds
|
||||
totalCompilationTime: number // in microseconds
|
||||
totalEvaluationTime: number // in microseconds
|
||||
|
||||
eventsByType: {
|
||||
compilation: number
|
||||
evaluation: number
|
||||
execution: number
|
||||
other: number
|
||||
}
|
||||
|
||||
scriptsByUrl: Map<string, {
|
||||
url: string
|
||||
events: JavaScriptEvent[]
|
||||
totalTime: number
|
||||
isThirdParty: boolean
|
||||
}>
|
||||
}
|
Loading…
Reference in New Issue
Block a user