Skip to content

Commit 9ad1d05

Browse files
committed
feat: regex details
1 parent ef1bca7 commit 9ad1d05

File tree

12 files changed

+250
-39
lines changed

12 files changed

+250
-39
lines changed

src/doctor.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,31 @@ import { dump } from './dump'
77
export class RegexDoctor {
88
map = new Map<RegExp, RecordRegexInfo>()
99

10+
duration = 0
11+
timeStart: number | undefined
12+
1013
constructor(
1114
public options: RegexDoctorOptions = {},
1215
) { }
1316

17+
private saveDuration(start: boolean = !!this.timeStart) {
18+
if (this.timeStart) {
19+
this.duration += performance.now() - this.timeStart
20+
this.timeStart = undefined
21+
}
22+
if (start)
23+
this.timeStart = performance.now()
24+
}
25+
1426
stop() {
27+
this.saveDuration(false)
1528
listeners.delete(this)
1629
if (!listeners.size)
1730
restore()
1831
}
1932

2033
start() {
34+
this.saveDuration(true)
2135
hijack()
2236
listeners.add(this)
2337

@@ -67,6 +81,7 @@ export class RegexDoctor {
6781
}
6882

6983
dump(options: RegexDoctorDumpOptions = {}): RegexDoctorResult {
70-
return dump(this.map, options)
84+
this.saveDuration()
85+
return dump(this, options)
7186
}
7287
}

src/dump.ts

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import type { StackFrameLite } from 'error-stack-parser-es/lite'
12
import { parseStack } from 'error-stack-parser-es/lite'
23
import { getTrace } from 'trace-record'
3-
import type { MergedRecordRegexInfo, RecordRegexInfo, RegexCallsDurations, RegexDoctorDumpFiltersOptions, RegexDoctorDumpOptions, RegexInfo } from './types'
4+
import type { MergedRecordRegexInfo, RecordRegexInfo, RegexCall, RegexCallsDurations, RegexDoctorDumpFiltersOptions, RegexDoctorDumpOptions, RegexDoctorResult, RegexInfo } from './types'
45
import { extractPackagePath, normalizeFilepath } from './shared/path'
6+
import type { RegexDoctor } from './doctor'
57

68
const defaultFilters: Required<RegexDoctorDumpFiltersOptions> = {
79
top: 20,
@@ -13,11 +15,13 @@ const defaultFilters: Required<RegexDoctorDumpFiltersOptions> = {
1315
}
1416

1517
export function dump(
16-
map: Map<RegExp, RecordRegexInfo>,
18+
doctor: RegexDoctor,
1719
options: RegexDoctorDumpOptions = {},
18-
) {
20+
): RegexDoctorResult {
21+
const map = doctor.map
1922
const {
2023
limitCalls = 5,
24+
limitInputLength = 500,
2125
// stacktrace = true,
2226
} = options
2327

@@ -26,7 +30,7 @@ export function dump(
2630
...defaultFilters,
2731
}
2832

29-
let totalDuration = 0
33+
let totalExecution = 0
3034
const uniqueMap = new Map<string, MergedRecordRegexInfo>()
3135

3236
Array.from(map.values())
@@ -60,7 +64,7 @@ export function dump(
6064
let infos = Array.from(uniqueMap.values())
6165
infos.forEach((info) => {
6266
info.durations = getDurations(info)
63-
totalDuration += info.durations.sum
67+
totalExecution += info.durations.sum
6468
})
6569
infos.sort((a, b) => b.durations!.sum - a.durations!.sum)
6670

@@ -85,20 +89,56 @@ export function dump(
8589
.sort((a, b) => b.duration - a.duration)
8690

8791
const files = new Set<string>()
92+
const traces = new Map<string, { idx: number, trace: StackFrameLite[] }>()
93+
94+
let infos = calls.map((call): RegexCall => {
95+
let traceIdx: number | undefined
96+
if (call.stack) {
97+
if (traces.has(call.stack)) {
98+
traceIdx = traces.get(call.stack)!.idx
99+
}
100+
else {
101+
const trace = parseStacktrace(call.stack)
102+
if (trace && trace[0]) {
103+
files.add(`${normalizeFilepath(trace[0].file!)}:${trace[0].line!}:${trace[0].col!}`)
104+
traceIdx = traces.size
105+
traces.set(call.stack, { idx: traceIdx, trace })
106+
}
107+
}
108+
}
88109

89-
let infos = calls.map((call) => {
90-
// TODO: group by unique stacks
91-
const trace = call.stack
92-
? parseStack(call.stack, { slice: [1, 10] }).filter(frame => frame.file)
93-
: undefined
94-
95-
if (trace && trace[0])
96-
files.add(`${normalizeFilepath(trace[0].file!)}:${trace[0].line!}:${trace[0].col!}`)
97-
110+
let input: RegexCall['input']
111+
if (call.input != null) {
112+
input = []
113+
if (call.input.length <= limitInputLength) {
114+
input.push(call.input)
115+
}
116+
else if (call.index != null) {
117+
const index = Math.max(0, Math.round(call.index - limitInputLength * 0.3))
118+
if (index > 0)
119+
input.push(index)
120+
const snippet = call.input.slice(index, index + limitInputLength)
121+
input.push(snippet)
122+
const rest = call.inputLength - index - snippet.length
123+
if (rest > 0)
124+
input.push(rest)
125+
}
126+
else {
127+
const snippet = call.input.slice(0, limitInputLength)
128+
input.push(snippet)
129+
const rest = call.inputLength - snippet.length
130+
if (rest > 0)
131+
input.push(rest)
132+
}
133+
}
98134
return {
99135
duration: call.duration,
100136
inputLength: call.inputLength,
101-
trace,
137+
input,
138+
trace: traceIdx,
139+
matched: call.matched,
140+
index: call.index,
141+
groups: call.groups,
102142
}
103143
})
104144

@@ -124,6 +164,9 @@ export function dump(
124164
copies: info.copies,
125165
calls: info.calls.length,
126166
callsInfos: infos,
167+
traces: Array.from(traces.values())
168+
.sort((a, b) => a.idx - b.idx)
169+
.map(({ trace }) => trace),
127170
durations: info.durations || getDurations(info),
128171
filesCalled,
129172
filesCreated: info.filesCreated,
@@ -135,7 +178,8 @@ export function dump(
135178
return {
136179
count: map.size,
137180
countUnique: uniqueMap.size,
138-
totalDuration,
181+
totalDuration: doctor.duration,
182+
totalExecution,
139183
regexInfos: infos.map((info, idx) => dumpInfo(info, idx)),
140184
cwd: options.cwd,
141185
}
@@ -161,3 +205,17 @@ function getDurations(info: RecordRegexInfo): RegexCallsDurations {
161205
max,
162206
}
163207
}
208+
209+
const traceCache: Map<string, StackFrameLite[] | undefined> = new Map()
210+
211+
function parseStacktrace(string?: string) {
212+
if (!string)
213+
return
214+
if (traceCache.has(string))
215+
return traceCache.get(string)
216+
const trace = string
217+
? parseStack(string, { slice: [1, 10] }).filter(frame => frame.file)
218+
: undefined
219+
traceCache.set(string, trace)
220+
return trace
221+
}

src/hijack.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,22 @@ export function hijack() {
4848
const result = _exec.call(this, string)
4949
const end = performance.now()
5050
const duration = end - start
51-
const call = Object.freeze(<RecordRegexCall>{
51+
const call: RecordRegexCall = {
5252
stack: duration > 0.001
5353
? new Error().stack
5454
: undefined,
5555
duration,
5656
input: string,
5757
inputLength: string.length,
58-
})
58+
}
59+
60+
if (result) {
61+
call.matched = true
62+
call.index = result.index
63+
call.groups = result.length - 1
64+
}
65+
66+
Object.freeze(call)
5967

6068
for (const listener of listeners) {
6169
const map = listener.map

src/types/data.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { StackFrameLite } from 'error-stack-parser-es/lite'
2+
import type { RecordRegexCall } from './record'
23

34
export interface RegexCallsDurations {
45
count: number
@@ -22,6 +23,10 @@ export interface RegexDoctorResult {
2223
* The number of unique regexes tracked
2324
*/
2425
countUnique: number
26+
/**
27+
* Total time spent in executing regexes
28+
*/
29+
totalExecution: number
2530
/**
2631
* Total time spent in all regexes
2732
*/
@@ -34,6 +39,10 @@ export interface RegexDoctorResult {
3439
* Working directory
3540
*/
3641
cwd?: string
42+
/**
43+
* CLI arguments
44+
*/
45+
argv?: string[]
3746
}
3847

3948
export interface RegexInfo {
@@ -53,6 +62,10 @@ export interface RegexInfo {
5362
* Notable call infos
5463
*/
5564
callsInfos: RegexCall[]
65+
/**
66+
* Array of stack traces
67+
*/
68+
traces?: (StackFrameLite[])[]
5669
/**
5770
* Number of calls
5871
*/
@@ -79,9 +92,13 @@ export interface RegexInfo {
7992
dynamic?: boolean
8093
}
8194

82-
export interface RegexCall {
83-
duration: number
84-
inputLength: number
85-
input?: string
86-
trace?: StackFrameLite[]
95+
export interface RegexCall extends Pick<RecordRegexCall, 'duration' | 'inputLength' | 'matched' | 'index' | 'groups'> {
96+
/**
97+
* Input string, number stands for the truncated length
98+
*/
99+
input?: (number | string)[]
100+
/**
101+
* Index of the stack trace map
102+
*/
103+
trace?: number
87104
}

src/types/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export interface RegexDoctorDumpOptions {
1313
*/
1414
limitCalls?: number
1515

16+
/**
17+
* Maximum length of the input to dump
18+
*
19+
* @default 500
20+
*/
21+
limitInputLength?: number
22+
1623
/**
1724
* Filters for regex details. Include if one of the requirements is met.
1825
*/

src/types/record.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export interface RecordRegexCall {
55
inputLength: number
66
input?: string
77
stack?: string
8+
matched?: boolean
9+
index?: number
10+
groups?: number
811
}
912

1013
export interface RecordRegexInfo {

ui/components/DurationDisplay.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
<script setup lang="ts">
2-
const props = defineProps<{
3-
ms: number
4-
}>()
2+
const props = withDefaults(
3+
defineProps<{
4+
ms: number
5+
colorful?: boolean
6+
}>(),
7+
{
8+
colorful: true,
9+
},
10+
)
511
612
const unit = ref('')
713
const number = ref(0)
@@ -31,6 +37,8 @@ watchEffect(() => {
3137
})
3238
3339
const color = computed(() => {
40+
if (props.colorful === false)
41+
return ''
3442
if (props.ms > 0.5)
3543
return 'text-red'
3644
if (props.ms > 0.2)

ui/components/PackageNameDisplay.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ const style = computed(() => ({
1414
</script>
1515

1616
<template>
17-
<code :style="style" px1 rounded text-sm>{{ name }}</code>
17+
<code :style="style" px1 rounded text-sm ws-nowrap>{{ name }}</code>
1818
</template>

0 commit comments

Comments
 (0)