Chromium 90, 给前端用的 devtools 的前端写得有性能问题
以此拗口又搞笑的标题纪念一下人生第一次给 Chromium 项目提的 issue,在学会了 tracing 工具的使用方式之后,也会有更多提 bug issue 的机会。
缘起
4月中旬后的一周里,感觉使用 Chrome Devtools 调试非常的卡,尤其是在刚打开 Devtools 的时候卡上七八秒。心情烦躁、血压飙升、严重影响了工作效率,提升了社会不稳定因素(大雾)。
定位问题
- 打开 Mac 的 Activity Monitor,明显发现打开 Devtools 时 Renderer 进程 CPU 飚上 100。
- 以上情形只发生在 Devtools 打开 Console tab 的时候,如果一开始并没有打开 Console,就没有卡顿。
- 仅在 Console tab 有大量 log 的情况下出现此问题,log 较少时正常。
- 使用 Vivaldi 浏览器没有问题,当时它基于 Chromium 89。Chrome 90 和同样基于 90 的 Edge 稳定版出现问题,因此把定位范围缩小到了 Chromium 90 的 Devtools 。
Devtools 本身就是用于前端调试和发现潜在性能问题的工具,那么我们用什么来调试它呢?
使用 chrome://tracing
Chromium 的 Tracing 工具 可以详细录制 Chromium 各个进程的运行时方法调用和占时,包括 C++ 和 Javascript。
chrome://tracing 除了录制以外,也是一个比较好用的 trace-viewer,可用来查看和分析由其他工具(例如 Android systrace, Electron 等)录制的 trace 数据。特别说一句,它也可以用来查看 Chrome Devtools Performance 录制的数据,但感觉 Viewer 的侧重不在 Web 开发的角度,少了一些对 Web Performance 特别定制的辅助可视化,没有 Devtools Performance 本身的查看器好用。
一些参考教程如下:
- Debugging our Graphics Stack - Google 幻灯片
- Chrome Tracing for Fun and Profit - Slack Engineering
- 强大的可视化利器 Chrome Trace Viewer 使用详解 - Limboy's HQ
开始录制
点开 chrome://tracing 页面左上角的 Record
按钮,然后
- 选择
Manually select settings
- 在
Record Categories
的几十个选项里,勾上devtools
- 点击
Record
开始录制 - 操作复现出 Console tab 的卡顿
下面是我随便找来的一张截图,展示一下 Record Categories
里的茫茫选项。里面的 v8/cc/blink 等,是前端工程师日常可以看一看的,可以从不同粒度了解一下 Chromium 一些运行时候的执行过程。当然这是建立在你对 Chromium 感兴趣和略有了解的基础上的,前端开发还是主要面向 Web 标准,日常还是应该使用没那么艰深的 Devtools,而不是一头扎入浏览器实现的细节中。
定位到问题
Devtools 的渲染主线程有触目惊心的近 6 秒卡顿,此时 V8 在执行 JS,看起来就🧐... 是前端的锅。
提 issue
去 Issues - chromium 提一个 issue,贴上截图、可重现问题的 html 文件、tracing 录制压缩包。
Chromium 缺陷追踪工具界面好难用,里里外外散发着 G 家工程向产品一贯的简陋风格。
后续
发现 chromium 91 已修复此问题
Devtools 的前端项目有一个自己的 repo,devtools/devtools-frontend - Git at Google,翻了一下发现这个 Gerrit Change 修复了此性能问题。
diff --git a/front_end/panels/console/ConsoleSidebar.ts b/front_end/panels/console/ConsoleSidebar.ts
index cd8ebaabc..3828d9ce5 100644
--- a/front_end/panels/console/ConsoleSidebar.ts
+++ b/front_end/panels/console/ConsoleSidebar.ts
@@ -164,13 +164,29 @@ export class URLGroupTreeElement extends ConsoleSidebarTreeElement {
}
}
+/**
+ * Maps the GroupName for a filter to the UIString used to render messages.
+ * Stored here so we only construct it once at runtime, rather than everytime we
+ * construct a filter or get a new message.
+ */
+const stringForFilterSidebarItemMap = new Map<GroupName, string>([
+ [GroupName.ConsoleAPI, UIStrings.dUserMessages],
+ [GroupName.All, UIStrings.dMessages],
+ [GroupName.Error, UIStrings.dErrors],
+ [GroupName.Warning, UIStrings.dWarnings],
+ [GroupName.Info, UIStrings.dInfo],
+ [GroupName.Verbose, UIStrings.dVerbose],
+]);
+
export class FilterTreeElement extends ConsoleSidebarTreeElement {
_selectedFilterSetting: Common.Settings.Setting<string>;
_urlTreeElements: Map<string|null, URLGroupTreeElement>;
_messageCount: number;
+ private uiStringForFilterCount: string;
constructor(filter: ConsoleFilter, icon: UI.Icon.Icon, selectedFilterSetting: Common.Settings.Setting<string>) {
super(filter.name, filter);
+ this.uiStringForFilterCount = stringForFilterSidebarItemMap.get(filter.name as GroupName) || '';
this._selectedFilterSetting = selectedFilterSetting;
this._urlTreeElements = new Map();
this.setLeadingIcons([icon]);
@@ -178,6 +194,7 @@ export class FilterTreeElement extends ConsoleSidebarTreeElement {
this._updateCounter();
}
+
clear(): void {
this._urlTreeElements.clear();
this.removeChildren();
@@ -195,21 +212,17 @@ export class FilterTreeElement extends ConsoleSidebarTreeElement {
}
_updateCounter(): void {
- this.title = this._updateGroupTitle(this._filter.name, this._messageCount);
+ this.title = this._updateGroupTitle(this._messageCount);
this.setExpandable(Boolean(this.childCount()));
}
- _updateGroupTitle(filterName: string, messageCount: number): string {
- const groupTitleMap = new Map([
- [GroupName.ConsoleAPI, i18nString(UIStrings.dUserMessages, {n: messageCount})],
- [GroupName.All, i18nString(UIStrings.dMessages, {n: messageCount})],
- [GroupName.Error, i18nString(UIStrings.dErrors, {n: messageCount})],
- [GroupName.Warning, i18nString(UIStrings.dWarnings, {n: messageCount})],
- [GroupName.Info, i18nString(UIStrings.dInfo, {n: messageCount})],
- [GroupName.Verbose, i18nString(UIStrings.dVerbose, {n: messageCount})],
- ]);
- return groupTitleMap.get(filterName as GroupName) || '';
+ _updateGroupTitle(messageCount: number): string {
+ if (this.uiStringForFilterCount) {
+ return i18nString(this.uiStringForFilterCount, {n: messageCount});
+ }
+ return '';
}
+
onMessageAdded(viewMessage: ConsoleViewMessage): void {
const message = viewMessage.consoleMessage();
const shouldIncrementCounter = message.type !== SDK.ConsoleModel.MessageType.Command &&
更多阅读
对 Devtools 项目有兴趣的,可以看一下他们的 engineering 博客和贡献指南。