diff --git a/src/vs/code/electron-browser/processExplorer/media/collapsed.svg b/src/vs/code/electron-browser/processExplorer/media/collapsed.svg new file mode 100755 index 00000000000..3a63808c358 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/media/collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/media/expanded.svg b/src/vs/code/electron-browser/processExplorer/media/expanded.svg new file mode 100755 index 00000000000..75f73adbb02 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/media/expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css index 2691208f2a2..b8c736e817c 100644 --- a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css @@ -88,3 +88,12 @@ td { tbody > tr:hover { background-color: #2A2D2E; } + +.hidden { + display: none; +} + +img { + width: 16px; + margin-right: 4px; +} \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index f2601c11312..ed19f54b2cd 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; -import { listProcesses } from 'vs/base/node/ps'; import { webFrame, ipcRenderer, clipboard } from 'electron'; import { repeat } from 'vs/base/common/strings'; import { totalmem } from 'os'; @@ -16,31 +15,45 @@ import * as platform from 'vs/base/common/platform'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; +import { addDisposableListener } from 'vs/base/browser/dom'; +import { IDisposable } from 'vs/base/common/lifecycle'; + -let processList: any[]; let mapPidToWindowTitle = new Map(); const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; +const listeners: IDisposable[] = []; +const collapsedStateCache: Map = new Map(); +let lastRequestTime: number; -function getProcessList(rootProcess: ProcessItem) { - const processes: any[] = []; +interface FormattedProcessItem { + cpu: string; + memory: string; + pid: string; + name: string; + formattedName: string; + cmd: string; +} + +function getProcessList(rootProcess: ProcessItem, isLocal: boolean): FormattedProcessItem[] { + const processes: FormattedProcessItem[] = []; if (rootProcess) { - getProcessItem(processes, rootProcess, 0); + getProcessItem(processes, rootProcess, 0, isLocal); } return processes; } -function getProcessItem(processes: any[], item: ProcessItem, indent: number): void { +function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean): void { const isRoot = (indent === 0); const MB = 1024 * 1024; let name = item.name; if (isRoot) { - name = `${product.applicationName} main`; + name = isLocal ? `${product.applicationName} main` : 'remote agent'; } if (name === 'window') { @@ -52,9 +65,9 @@ function getProcessItem(processes: any[], item: ProcessItem, indent: number): vo const formattedName = isRoot ? name : `${repeat(' ', indent)} ${name}`; const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); processes.push({ - cpu: Number(item.load.toFixed(0)), - memory: Number((memory / MB).toFixed(0)), - pid: Number((item.pid).toFixed(0)), + cpu: item.load.toFixed(0), + memory: (memory / MB).toFixed(0), + pid: item.pid.toFixed(0), name, formattedName, cmd: item.cmd @@ -62,7 +75,7 @@ function getProcessItem(processes: any[], item: ProcessItem, indent: number): vo // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => getProcessItem(processes, child, indent + 1)); + item.children.forEach(child => getProcessItem(processes, child, indent + 1, isLocal)); } } @@ -71,7 +84,7 @@ function isDebuggable(cmd: string): boolean { return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; } -function attachTo(item: ProcessItem) { +function attachTo(item: FormattedProcessItem) { const config: any = { type: 'node', request: 'attach', @@ -113,35 +126,57 @@ function getProcessIdWithHighestProperty(processList: any[], propertyName: strin return maxProcessId; } -function updateProcessInfo(processList: any[]): void { +function updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: HTMLImageElement, sectionName: string) { + if (shouldExpand) { + body.classList.remove('hidden'); + collapsedStateCache.set(sectionName, false); + twistie.src = './media/expanded.svg'; + } else { + body.classList.add('hidden'); + collapsedStateCache.set(sectionName, true); + twistie.src = './media/collapsed.svg'; + } +} + +function renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { const container = document.getElementById('process-list'); if (!container) { return; } - container.innerHTML = ''; const highestCPUProcess = getProcessIdWithHighestProperty(processList, 'cpu'); const highestMemoryProcess = getProcessIdWithHighestProperty(processList, 'memory'); - const tableHead = document.createElement('thead'); - tableHead.innerHTML = ` - ${localize('cpu', "CPU %")} - ${localize('memory', "Memory (MB)")} - ${localize('pid', "pid")} - ${localize('name', "Name")} - `; + const body = document.createElement('tbody'); - const tableBody = document.createElement('tbody'); + if (renderManySections) { + const headerRow = document.createElement('tr'); + const data = document.createElement('td'); + data.textContent = sectionName; + data.colSpan = 4; + headerRow.appendChild(data); + + const twistie = document.createElement('img'); + updateSectionCollapsedState(!collapsedStateCache.get(sectionName), body, twistie, sectionName); + data.prepend(twistie); + + listeners.push(addDisposableListener(data, 'click', (e) => { + const isHidden = body.classList.contains('hidden'); + updateSectionCollapsedState(isHidden, body, twistie, sectionName); + })); + + container.appendChild(headerRow); + } processList.forEach(p => { const row = document.createElement('tr'); - row.id = p.pid; + row.id = p.pid.toString(); const cpu = document.createElement('td'); p.pid === highestCPUProcess ? cpu.classList.add('centered', 'highest') : cpu.classList.add('centered'); - cpu.textContent = p.cpu; + cpu.textContent = p.cpu.toString(); const memory = document.createElement('td'); p.pid === highestMemoryProcess @@ -160,10 +195,41 @@ function updateProcessInfo(processList: any[]): void { name.textContent = p.formattedName; row.append(cpu, memory, pid, name); - tableBody.appendChild(row); + + listeners.push(addDisposableListener(row, 'contextmenu', (e) => { + showContextMenu(e, p, sectionIsLocal); + })); + + body.appendChild(row); }); - container.append(tableHead, tableBody); + container.appendChild(body); +} + +function updateProcessInfo(processLists: { [key: string]: ProcessItem }): void { + const container = document.getElementById('process-list'); + if (!container) { + return; + } + + container.innerHTML = ''; + listeners.forEach(l => l.dispose()); + + const tableHead = document.createElement('thead'); + tableHead.innerHTML = ` + ${localize('cpu', "CPU %")} + ${localize('memory', "Memory (MB)")} + ${localize('pid', "pid")} + ${localize('name', "Name")} + `; + + container.append(tableHead); + + const hasMultipleMachines = Object.keys(processLists).length > 1; + Object.keys(processLists).forEach((key, i) => { + const isLocal = i === 0; + renderTableSection(key, getProcessList(processLists[key], isLocal), hasMultipleMachines, isLocal); + }); } function applyStyles(styles: ProcessExplorerStyles): void { @@ -171,11 +237,11 @@ function applyStyles(styles: ProcessExplorerStyles): void { const content: string[] = []; if (styles.hoverBackground) { - content.push(`tbody > tr:hover { background-color: ${styles.hoverBackground}; }`); + content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { - content.push(`tbody > tr:hover{ color: ${styles.hoverForeground}; }`); + content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); } if (styles.highlightForeground) { @@ -200,13 +266,13 @@ function applyZoom(zoomLevel: number): void { browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); } -function showContextMenu(e: MouseEvent) { +function showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { e.preventDefault(); const items: IContextMenuItem[] = []; + const pid = Number(item.pid); - const pid = parseInt((e.currentTarget as HTMLElement).id); - if (pid && typeof pid === 'number') { + if (isLocal) { items.push({ label: localize('killProcess', "Kill Process"), click() { @@ -224,48 +290,37 @@ function showContextMenu(e: MouseEvent) { items.push({ type: 'separator' }); + } - items.push({ - label: localize('copy', "Copy"), - click() { - const row = document.getElementById(pid.toString()); - if (row) { - clipboard.writeText(row.innerText); - } + items.push({ + label: localize('copy', "Copy"), + click() { + const row = document.getElementById(pid.toString()); + if (row) { + clipboard.writeText(row.innerText); } - }); - - items.push({ - label: localize('copyAll', "Copy All"), - click() { - const processList = document.getElementById('process-list'); - if (processList) { - clipboard.writeText(processList.innerText); - } - } - }); - - const item = processList.filter(process => process.pid === pid)[0]; - if (item && isDebuggable(item.cmd)) { - items.push({ - type: 'separator' - }); - - items.push({ - label: localize('debug', "Debug"), - click() { - attachTo(item); - } - }); } - } else { + }); + + items.push({ + label: localize('copyAll', "Copy All"), + click() { + const processList = document.getElementById('process-list'); + if (processList) { + clipboard.writeText(processList.innerText); + } + } + }); + + if (item && isLocal && isDebuggable(item.cmd)) { items.push({ - label: localize('copyAll', "Copy All"), + type: 'separator' + }); + + items.push({ + label: localize('debug', "Debug"), click() { - const processList = document.getElementById('process-list'); - if (processList) { - clipboard.writeText(processList.innerText); - } + attachTo(item); } }); } @@ -273,6 +328,22 @@ function showContextMenu(e: MouseEvent) { popup(items); } +function requestProcessList(totalWaitTime: number): void { + setTimeout(() => { + const nextRequestTime = Date.now(); + const waited = totalWaitTime + nextRequestTime - lastRequestTime; + lastRequestTime = nextRequestTime; + + // Wait at least a second between requests. + if (waited > 1000) { + ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:listProcesses'); + } else { + requestProcessList(waited); + } + }, 200); +} + export function startup(data: ProcessExplorerData): void { applyStyles(data.styles); applyZoom(data.zoomLevel); @@ -283,23 +354,14 @@ export function startup(data: ProcessExplorerData): void { windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); }); - setInterval(() => { - ipcRenderer.send('windowsInfoRequest'); - - listProcesses(data.pid).then(processes => { - processList = getProcessList(processes); - updateProcessInfo(processList); - - const tableRows = document.getElementsByTagName('tr'); - for (let i = 0; i < tableRows.length; i++) { - const tableRow = tableRows[i]; - tableRow.addEventListener('contextmenu', (e) => { - showContextMenu(e); - }); - } - }); - }, 1200); + ipcRenderer.on('vscode:listProcessesResponse', (_event: Event, processRoots: { [key: string]: ProcessItem }) => { + updateProcessInfo(processRoots); + requestProcessList(0); + }); + lastRequestTime = Date.now(); + ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:listProcesses'); document.onkeydown = (e: KeyboardEvent) => { const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts index 08eb0472e0b..e5bc282fb55 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts @@ -306,13 +306,13 @@ export class DiagnosticsService implements IDiagnosticsService { output.push('CPU %\tMem MB\t PID\tProcess'); if (rootProcess) { - this.formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0); + this.formatProcessItem(info.mainPID, mapPidToWindowTitle, output, rootProcess, 0); } return output.join('\n'); } - private formatProcessItem(mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { + private formatProcessItem(mainPid: number, mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { const isRoot = (indent === 0); const MB = 1024 * 1024; @@ -320,7 +320,7 @@ export class DiagnosticsService implements IDiagnosticsService { // Format name with indent let name: string; if (isRoot) { - name = `${product.applicationName} main`; + name = item.pid === mainPid ? `${product.applicationName} main` : 'remote agent'; } else { name = `${repeat(' ', indent)} ${item.name}`; @@ -333,7 +333,7 @@ export class DiagnosticsService implements IDiagnosticsService { // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => this.formatProcessItem(mapPidToWindowTitle, output, child, indent + 1)); + item.children.forEach(child => this.formatProcessItem(mainPid, mapPidToWindowTitle, output, child, indent + 1)); } } } diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 5e9b24afe81..63ef9c2edff 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -15,6 +15,8 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; +import { listProcesses } from 'vs/base/node/ps'; +import { ProcessItem } from 'vs/base/common/processes'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; @@ -44,6 +46,16 @@ export class IssueService implements IIssueService { }); }); + ipcMain.on('vscode:listProcesses', async (event: Event) => { + const mainPid = await this.launchService.getMainProcessId(); + const rootProcess = await listProcesses(mainPid); + const remoteProcesses = (await this.launchService.getRemoteDiagnostics({ includeProcesses: true })) + .map(data => data.processes) + .filter((x): x is ProcessItem => !!x); + + event.sender.send('vscode:listProcessesResponse', [rootProcess, ...remoteProcesses]); + }); + ipcMain.on('vscode:issuePerformanceInfoRequest', (event: Event) => { this.getPerformanceInfo().then(msg => { event.sender.send('vscode:issuePerformanceInfoResponse', msg);