1
+ /* eslint-disable node/no-unsupported-features/node-builtins */
1
2
/*---------------------------------------------------------
2
3
* Copyright 2021 The Go Authors. All rights reserved.
3
4
* Licensed under the MIT License. See LICENSE in the project root for license information.
@@ -11,16 +12,30 @@ import {
11
12
TreeDataProvider ,
12
13
TreeItem ,
13
14
TreeItemCollapsibleState ,
14
- Uri
15
+ Uri ,
16
+ ViewColumn
15
17
} from 'vscode' ;
16
18
import vscode = require( 'vscode' ) ;
17
- import { getTempFilePath } from '../util' ;
19
+ import { promises as fs } from 'fs' ;
20
+ import { ChildProcess , spawn } from 'child_process' ;
21
+ import { getBinPath , getTempFilePath } from '../util' ;
18
22
import { GoTestResolver } from './resolve' ;
23
+ import { killProcessTree } from '../utils/processUtils' ;
24
+ import { correctBinname } from '../utils/pathUtils' ;
19
25
20
26
export type ProfilingOptions = { kind ?: Kind [ 'id' ] } ;
21
27
22
28
const optionsMemento = 'testProfilingOptions' ;
23
29
const defaultOptions : ProfilingOptions = { kind : 'cpu' } ;
30
+ const pprofProcesses = new Set < ChildProcess > ( ) ;
31
+
32
+ export function killRunningPprof ( ) {
33
+ return new Promise < boolean > ( ( resolve ) => {
34
+ pprofProcesses . forEach ( ( proc ) => killProcessTree ( proc ) ) ;
35
+ pprofProcesses . clear ( ) ;
36
+ resolve ( true ) ;
37
+ } ) ;
38
+ }
24
39
25
40
export class GoTestProfiler {
26
41
public readonly view = new ProfileTreeDataProvider ( this ) ;
@@ -41,9 +56,8 @@ export class GoTestProfiler {
41
56
const kind = Kind . get ( options . kind ) ;
42
57
if ( ! kind ) return [ ] ;
43
58
44
- const flags = [ ] ;
45
59
const run = new File ( kind , item ) ;
46
- flags . push ( run . flag ) ;
60
+ const flags = [ ... run . flags ] ;
47
61
if ( this . runs . has ( item . id ) ) this . runs . get ( item . id ) . unshift ( run ) ;
48
62
else this . runs . set ( item . id , [ run ] ) ;
49
63
return flags ;
@@ -54,7 +68,7 @@ export class GoTestProfiler {
54
68
vscode . commands . executeCommand ( 'setContext' , 'go.profiledTests' , Array . from ( this . runs . keys ( ) ) ) ;
55
69
vscode . commands . executeCommand ( 'setContext' , 'go.hasProfiles' , this . runs . size > 0 ) ;
56
70
57
- this . view . didRun ( ) ;
71
+ this . view . fireDidChange ( ) ;
58
72
}
59
73
60
74
hasProfileFor ( id : string ) : boolean {
@@ -75,7 +89,23 @@ export class GoTestProfiler {
75
89
} ;
76
90
}
77
91
78
- async showProfiles ( item : TestItem ) {
92
+ async delete ( file : File ) {
93
+ await file . delete ( ) ;
94
+
95
+ const runs = this . runs . get ( file . target . id ) ;
96
+ if ( ! runs ) return ;
97
+
98
+ const i = runs . findIndex ( ( x ) => x === file ) ;
99
+ if ( i < 0 ) return ;
100
+
101
+ runs . splice ( i , 1 ) ;
102
+ if ( runs . length === 0 ) {
103
+ this . runs . delete ( file . target . id ) ;
104
+ }
105
+ this . view . fireDidChange ( ) ;
106
+ }
107
+
108
+ async show ( item : TestItem ) {
79
109
const { query : kind , fragment : name } = Uri . parse ( item . id ) ;
80
110
if ( kind !== 'test' && kind !== 'benchmark' && kind !== 'example' ) {
81
111
await vscode . window . showErrorMessage ( 'Selected item is not a test, benchmark, or example' ) ;
@@ -110,6 +140,85 @@ export class GoTestProfiler {
110
140
}
111
141
}
112
142
143
+ async function show ( profile : string ) {
144
+ const foundDot = await new Promise < boolean > ( ( resolve , reject ) => {
145
+ const proc = spawn ( correctBinname ( 'dot' ) , [ '-V' ] ) ;
146
+
147
+ proc . on ( 'error' , ( err ) => {
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ if ( ( err as any ) . code === 'ENOENT' ) resolve ( false ) ;
150
+ else reject ( err ) ;
151
+ } ) ;
152
+
153
+ proc . on ( 'exit' , ( code , signal ) => {
154
+ if ( signal ) reject ( new Error ( `Received signal ${ signal } ` ) ) ;
155
+ else if ( code ) reject ( new Error ( `Exited with code ${ code } ` ) ) ;
156
+ else resolve ( true ) ;
157
+ } ) ;
158
+ } ) ;
159
+ if ( ! foundDot ) {
160
+ const r = await vscode . window . showErrorMessage (
161
+ 'Failed to execute dot. Is Graphviz installed?' ,
162
+ 'Open graphviz.org'
163
+ ) ;
164
+ if ( r ) await vscode . env . openExternal ( vscode . Uri . parse ( 'https://graphviz.org/' ) ) ;
165
+ return ;
166
+ }
167
+
168
+ const proc = spawn ( getBinPath ( 'go' ) , [ 'tool' , 'pprof' , '-http=:' , '-no_browser' , profile ] ) ;
169
+ pprofProcesses . add ( proc ) ;
170
+
171
+ const port = await new Promise < string > ( ( resolve , reject ) => {
172
+ proc . on ( 'error' , ( err ) => {
173
+ pprofProcesses . delete ( proc ) ;
174
+ reject ( err ) ;
175
+ } ) ;
176
+
177
+ proc . on ( 'exit' , ( code , signal ) => {
178
+ pprofProcesses . delete ( proc ) ;
179
+ reject ( signal || code ) ;
180
+ } ) ;
181
+
182
+ let stderr = '' ;
183
+ function captureStdout ( b : Buffer ) {
184
+ stderr += b . toString ( 'utf-8' ) ;
185
+
186
+ const m = stderr . match ( / ^ S e r v i n g w e b U I o n h t t p : \/ \/ l o c a l h o s t : (?< port > \d + ) \n / ) ;
187
+ if ( ! m ) return ;
188
+
189
+ resolve ( m . groups . port ) ;
190
+ proc . stdout . off ( 'data' , captureStdout ) ;
191
+ }
192
+
193
+ proc . stderr . on ( 'data' , captureStdout ) ;
194
+ } ) ;
195
+
196
+ const panel = vscode . window . createWebviewPanel ( 'go.profile' , 'Profile' , ViewColumn . Active ) ;
197
+ panel . webview . options = { enableScripts : true } ;
198
+ panel . webview . html = `<html>
199
+ <head>
200
+ <style>
201
+ body {
202
+ padding: 0;
203
+ background: white;
204
+ overflow: hidden;
205
+ }
206
+
207
+ iframe {
208
+ border: 0;
209
+ width: 100%;
210
+ height: 100vh;
211
+ }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <iframe src="http://localhost:${ port } "></iframe>
216
+ </body>
217
+ </html>` ;
218
+
219
+ panel . onDidDispose ( ( ) => killProcessTree ( proc ) ) ;
220
+ }
221
+
113
222
class Kind {
114
223
private static byID = new Map < string , Kind > ( ) ;
115
224
@@ -143,24 +252,30 @@ class File {
143
252
144
253
constructor ( public readonly kind : Kind , public readonly target : TestItem ) { }
145
254
255
+ async delete ( ) {
256
+ return Promise . all (
257
+ [ getTempFilePath ( `${ this . name } .prof` ) , getTempFilePath ( `${ this . name } .test` ) ] . map ( ( file ) => fs . unlink ( file ) )
258
+ ) ;
259
+ }
260
+
146
261
get label ( ) {
147
262
return `${ this . kind . label } @ ${ this . when . toTimeString ( ) } ` ;
148
263
}
149
264
150
265
get name ( ) {
151
- return `profile-${ this . id } .${ this . kind . id } .prof ` ;
266
+ return `profile-${ this . id } .${ this . kind . id } ` ;
152
267
}
153
268
154
- get flag ( ) : string {
155
- return ` ${ this . kind . flag } = ${ getTempFilePath ( this . name ) } ` ;
269
+ get flags ( ) : string [ ] {
270
+ return [ this . kind . flag , getTempFilePath ( ` ${ this . name } .prof` ) , '-o' , getTempFilePath ( ` ${ this . name } .test` ) ] ;
156
271
}
157
272
158
- get uri ( ) : Uri {
159
- return Uri . from ( { scheme : 'go-tool-pprof' , path : getTempFilePath ( this . name ) } ) ;
273
+ get uri ( ) {
274
+ return Uri . file ( getTempFilePath ( ` ${ this . name } .prof` ) ) ;
160
275
}
161
276
162
277
async show ( ) {
163
- await vscode . window . showTextDocument ( this . uri ) ;
278
+ await show ( getTempFilePath ( ` ${ this . name } .prof` ) ) ;
164
279
}
165
280
}
166
281
@@ -172,14 +287,14 @@ class ProfileTreeDataProvider implements TreeDataProvider<TreeElement> {
172
287
173
288
constructor ( private readonly profiler : GoTestProfiler ) { }
174
289
175
- didRun ( ) {
290
+ fireDidChange ( ) {
176
291
this . didChangeTreeData . fire ( ) ;
177
292
}
178
293
179
294
getTreeItem ( element : TreeElement ) : TreeItem {
180
295
if ( element instanceof File ) {
181
296
const item = new TreeItem ( element . label ) ;
182
- item . contextValue = 'file' ;
297
+ item . contextValue = 'go:test: file' ;
183
298
item . command = {
184
299
title : 'Open' ,
185
300
command : 'vscode.open' ,
@@ -189,7 +304,7 @@ class ProfileTreeDataProvider implements TreeDataProvider<TreeElement> {
189
304
}
190
305
191
306
const item = new TreeItem ( element . label , TreeItemCollapsibleState . Collapsed ) ;
192
- item . contextValue = 'test' ;
307
+ item . contextValue = 'go:test: test' ;
193
308
const options : TextDocumentShowOptions = {
194
309
preserveFocus : false ,
195
310
selection : new Range ( element . range . start , element . range . start )
0 commit comments