1 | # Architecture of DevTools
|
2 |
|
3 | This document explains the high-level architecture as well as any considerations that were made along the way.
|
4 | The document is evolving and will be updated whenever architectural changes are being made.
|
5 |
|
6 | ## Guiding principles
|
7 |
|
8 | Throughout this document, references are included to relevant [Web Platform Design Principles], whenever they are applicable for that specific section.
|
9 | It is recommended to be familiar with the Web Platform Design Principles prior to reading to this document, but it is not required.
|
10 | There are additional DevTools-specific guiding principles that are listed in this section.
|
11 |
|
12 | ### Load only what is necessary
|
13 |
|
14 | DevTools is a large web application.
|
15 | It contains dozens of features, most of them are distinct.
|
16 | As such, loading all features up front is infeasible and can lead to large startup times of DevTools.
|
17 | The DevTools architecture should encourage granular implementations of features, lazy loading the minimal amount of code to make features work.
|
18 |
|
19 | See also:
|
20 | * [Put user needs first]
|
21 |
|
22 | ### Prefer web platform features whenever possible
|
23 |
|
24 | DevTools ships as part of Chromium-based browsers and therefore is long-living.
|
25 | Code that is shipped today can live on for years, even decades.
|
26 | Therefore, web best practices constantly evolve during the lifespan of DevTools.
|
27 | To avoid frequent rewrites of features, each feature should be implemented with longevity in mind.
|
28 | Web platform features are standardized and designed to be supported ad infinitum.
|
29 | Whenever possible, prefer the usage of web platform features over custom solutions, as custom solutions require constant maintenance and are more likely to become out-of-date.
|
30 |
|
31 | See also:
|
32 | * [Prefer simple solutions]
|
33 | * [Put user needs first]
|
34 |
|
35 | ### Design with continuous deployment in mind
|
36 |
|
37 | DevTools ships every single day in Canary builds of Chromium-based browsers.
|
38 | It is therefore risky to halt development during a migration (even for a couple of weeks), as DevTools can cause Canary builds to break and effect not just end-users, but also engineers working on the web platform itself.
|
39 | The symbiosis of the web platform and DevTools means that DevTools itself must be kept up-to-date, to support a continuously evolving platform.
|
40 |
|
41 | Migrations should therefore be gradual and allow for continuous deployment of DevTools in Canary builds.
|
42 | The migrations will thus have to take into account not just the desired end solution, but also the limitations of today's implementation.
|
43 | In the end, it is not possible to predict when migrations are completed, which means that the codebase can be under migration for a significant amount of time.
|
44 | Ensure that migrations do not (strongly) negatively impact feature development and evolution of the wider web platform and can be completed in a timely fashion.
|
45 |
|
46 | See also:
|
47 | * [Put user needs first]
|
48 | * [Prefer web platform features whenever possible]
|
49 |
|
50 | ### Use flexible third_party libraries whenever necessary
|
51 |
|
52 | Not all requirements of DevTools can be fulfilled by web platform features alone.
|
53 | There will be situations in which third_party libraries (ideally closely built on top of web platform features) are the appropriate solution.
|
54 | Every third_party library introduced in DevTools adds risk to the longterm maintenance of the overall product.
|
55 | Therefore, each third_party library that DevTools uses should be flexible: a library should be (relatively) easy to be removed from the product.
|
56 |
|
57 | In practice, this means that any new third_party library must allow for gradual introduction in the codebase, but (if required) also gradually removal.
|
58 | Since third_party libraries can become unmaintained, gradual removal allows continued development of DevTools features, while the impact of the deprecation is dealt with.
|
59 | If a third_party library is difficult to remove and has a broad impact on the overall codebase, it could cause a halt of development of DevTools features.
|
60 | Since the web platform is continuously evolving and DevTools is a part of the platform focused on web developers, halting feature development can have a negative impact on the wider web platform.
|
61 |
|
62 | Concretely, the introduction of a framework that takes control of the lifecycle of (parts of) DevTools is practically impossible.
|
63 | Such frameworks require difficult-to-execute migrations and typically don't allow for gradual removals.
|
64 | Moreover, decisions made by maintainers of third_party frameworks could cause significant maintenance churn for DevTools maintainers.
|
65 |
|
66 | Note that in this section, the definition of "framework" can differ based on point-of-views of stakeholders and could apply more broadly than initially expected.
|
67 | Make sure to evaluate third_party packages based on impact on the DevTools codebase, which could be larger than third_party maintainers might have intended.
|
68 | In other words: even if a third_party package is advertised as a library, it could still be considered as a framework from the perspective of DevTools maintainers.
|
69 |
|
70 | See also:
|
71 | * [Load only what is necessary]
|
72 | * [Design with continuous deployment in mind]
|
73 |
|
74 | ### Limit implementation possibilities while providing maximum flexibility
|
75 |
|
76 | Typically, there are multiples ways to implement application features on the web.
|
77 | A direct result of the flexibility of the web is the proliferation of different solutions to the same problem.
|
78 | A negative consequence of the flexibility is the wide variety of solutions and corresponding maintenance cost in the longterm future.
|
79 | The DevTools architecture should limit the amount of possible solutions to various problems, yet providing maximum flexibility to engineers implementing DevTools features.
|
80 |
|
81 | Sadly, that is easier said than done.
|
82 | Even when taking this principle into account when working on DevTools' architecture, it can be relatively easy to discover "architectural regressions" years later.
|
83 | On the flipside, it can be appealing to be overly restrictive, to avoid such "architectural regressions".
|
84 | However, "unnecessary" (this qualification can be subjective and differ based on point-of-view) restrictions can have a strong negative impact on development of DevTools features and therefore can cause more problems on its own.
|
85 |
|
86 | Balancing the architectural requirements to ensure a stable and fast-loading DevTools versus the needs of implementing new DevTools features is a continuously evolving process.
|
87 | To ensure a healthy balance, a periodic evaluation can be useful to address potential architectural technical debt.
|
88 |
|
89 | See also:
|
90 | * [Prefer simple solutions]
|
91 | * [Load only what is necessary]
|
92 |
|
93 | # Build system
|
94 |
|
95 | Since DevTools is a complex application that integrates with Chromium, it is built on top of the Chromium build system [GN] built on top of [Ninja].
|
96 | The build system ensures that all relevant files are correctly defined for consumption by Chromium.
|
97 | It also integrates with (third_party) tooling such as a type checker ([TypeScript]) and upstream Chromium packages ([Chrome DevTools Protocol]).
|
98 |
|
99 | DevTools-specific GN templates are defined in [scripts/build/ninja/](scripts/build/ninja/).
|
100 | Chromium consumes the output of the DevTools GN target [:generate_devtools_grd defined in BUILD.gn](BUILD.gn), which generates a GRD bundle using [GRIT].
|
101 | Detailed descriptions about each template are included in [scripts/build/ninja/README.md](scripts/build/ninja/README.md).
|
102 |
|
103 | # Startup process overview
|
104 |
|
105 | DevTools startup process is based on a core-feature model.
|
106 | The application builds on top of a core of common functionality that is shared between features.
|
107 | The core is responsible for communicating with the Chromium backend and building a foundation of UI functionality to facilitate the definition of higher-level panels containing features.
|
108 | Each feature is declared upfront and lazily loaded whenever the user interacts with the feature [[Load only what is necessary]].
|
109 |
|
110 | Loading of core functionality and features is built on top of [JavaScript modules].
|
111 | Core functionality is loaded via static imports, while implementations of features is lazily loaded using [dynamic imports].
|
112 | Features themselves use static imports for loading core and feature-specific functionality.
|
113 |
|
114 | ![A high-level overview of how feature implementations are lazily loaded in the DevTools startup process](./docs/images/architecture-lazy-loading-features.png)
|
115 |
|
116 | Enforcement of the rules regarding loading is implemented using the [ESLint] rule defined in [scripts/eslint_rules/lib/es_modules_import.js](scripts/eslint_rules/lib/es_modules_import.js).
|
117 |
|
118 | ## DevTools application entrypoints
|
119 |
|
120 | There are multiple variants of the DevTools application.
|
121 | The main DevTools application is [front_end/devtools_app.js](front_end/devtools_app/devtools_app.js), which developers can open via F12 or "Right click -> Inspect element" in their Chromium browser.
|
122 | Other application entrypoints are used when debugging via Node ([front_end/node_app.js](front_end/node_app/node_app.js)), a slimmed down V8-specific Node debugging context ([front_end/js_app.js](front_end/js_app/js_app.js)), a (service) worker context ([front_end/worker_app.js](front_end/worker_app/worker_app.js)), a standalone browser window ([front_end/toolbox.ts](front_end/toolbox/toolbox.ts)), remote debugging of (Android) devices with `chrome://inspect/#devices` ([front_end/inspector.js](front_end/inspector/inspector.js)), standalone Node debugger [ndb] ([front_end/ndb_app.js](front_end/ndb_app/ndb_app.js)) and a legacy entrypoint for Chromium layout tests ([front_end/integration_test_runner/integration_test_runner.js](front_end/integration_test_runner.js)).
|
123 |
|
124 | Each application entrypoint has a corresponding `.html` file with the same name, that can be loaded by the Chromium DevTools backend.
|
125 | The JavaScript files import the relevant "meta" files containing the declarations of DevTools features.
|
126 |
|
127 | ## Upfront declaration of DevTools features
|
128 |
|
129 | The upfront declaration of DevTools features is also known as "extensions".
|
130 | Extensions add functionality to the DevTools application using declarative registration calls.
|
131 |
|
132 | There are multiple types of extensions, including how DevTools handles its own internal business logic or to declare user-facing features with localized strings.
|
133 | There are 4 main types of extensions:
|
134 |
|
135 | * [UI.ActionRegistration.Action](front_end/ui/ActionRegistration.ts)
|
136 | * [UI.View.View](front_end/ui/View.js)
|
137 | * [Common.Settings.Setting](front_end/common/Settings.js)
|
138 | * General type lookups.
|
139 |
|
140 | Each specific extension is documented in the README of their respective folder.
|
141 |
|
142 | The registration of a particular extension implemented in `module` must always be declared in a `<module>-meta.ts` in the same folder.
|
143 | The meta files should be included by all relevant DevTools application entrypoints.
|
144 | If you want to make functionality available in all DevTools application entrypoints, you can import it in [shell.js](front_end/shell.js).
|
145 |
|
146 | For example, the meta declaration file for [front_end/network/](front_end/network/) is called [front_end/network/network-meta.ts](front_end/network/network-meta.ts) and defined in [front_end/network/BUILD.gn](front_end/network/BUILD.gn):
|
147 |
|
148 | ```python
|
149 | devtools_entrypoint("meta") {
|
150 | entrypoint = "network-meta.ts"
|
151 | deps = [
|
152 | ":bundle",
|
153 | "../root:bundle",
|
154 | "../ui:bundle",
|
155 | ]
|
156 | }
|
157 | ```
|
158 |
|
159 | Below is an example implementation of a `<module>-meta.ts` (using [front_end/network/network-meta.ts](front_end/network/network-meta.ts) as running example).
|
160 | For information about the localization system, please see the documentation in [docs/localization/](docs/localization/).
|
161 |
|
162 | ```ts
|
163 | // Any static imports on core modules
|
164 | import * as Common from '../common/common.js';
|
165 | import * as UI from '../ui/ui.js';
|
166 |
|
167 | // A type-import that is removed during compilation by TypeScript. Therefore,
|
168 | // it is not a static import on runtime and adheres to the lazy loading
|
169 | // rules defined for the startup process.
|
170 | import type * as Network from './network.js';
|
171 |
|
172 | import * as i18n from '../i18n/i18n.js';
|
173 | const UIStrings = {
|
174 | // UIStrings definitions here
|
175 | };
|
176 | const str_ = i18n.i18n.registerUIStrings('network/network-meta.ts', UIStrings);
|
177 | // Since meta files are loaded synchronously during startup, the localization system
|
178 | // has not finished loading yet and we need to lazily compute localized strings.
|
179 | const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
|
180 | // The result of the dynamically loaded module. Will be undefined until the user has
|
181 | // started using a feature defined in this module.
|
182 | let loadedNetworkModule: (typeof Network|undefined);
|
183 |
|
184 | // Lazily load the functionality for the network panel. This function will only dynamically
|
185 | // import the module once.
|
186 | async function loadNetworkModule(): Promise<typeof Network> {
|
187 | if (!loadedNetworkModule) {
|
188 | // Dynamic import to load `front_end/network/network.ts`, which contains the
|
189 | // actual implementation of the Network panel.
|
190 | loadedNetworkModule = await import('./network.js');
|
191 | }
|
192 | return loadedNetworkModule;
|
193 | }
|
194 |
|
195 | // Retrieve any context types from the lazily loaded module, but only if the module has
|
196 | // already been loaded at least once. The context types are used to determine the availibility
|
197 | // of certain DevTools extensions, for example certain feature-specific command menu entries
|
198 | // like the debugger actions when debugging source code. For more information, see the usage
|
199 | // of this function down below.
|
200 | function maybeRetrieveContextTypes<T = unknown>(getClassCallBack: (loadedNetworkModule: typeof Network) => T[]): T[] {
|
201 | if (loadedNetworkModule === undefined) {
|
202 | return [];
|
203 | }
|
204 | return getClassCallBack(loadedNetworkModule);
|
205 | }
|
206 |
|
207 | // A top-level panel. This is the main extension for a high-level feature. The panel is
|
208 | // loaded whenever the user clicks on the panel tab or loads it via the "More Tools" menu.
|
209 | // For more information about view extensions, please see the documentation in
|
210 | // `front_end/ui/View.js`.
|
211 | UI.ViewManager.registerViewExtension({
|
212 | location: UI.ViewManager.ViewLocationValues.PANEL,
|
213 | id: 'network',
|
214 | commandPrompt: i18nLazyString(UIStrings.showNetwork),
|
215 | title: i18nLazyString(UIStrings.network),
|
216 | order: 40,
|
217 | // This function is executed by the `ViewManager` (defined in `front_end/ui/ViewManager.js`)
|
218 | // whenever the user requests the panel to be loaded.
|
219 | async loadView() {
|
220 | // Lazily load the module, if we hadn't loaded it already.
|
221 | const Network = await loadNetworkModule();
|
222 | // Obtain a singleton reference to the network panel. We don't allow multiple instances
|
223 | // of the same panel, to ensure a performant application.
|
224 | return Network.NetworkPanel.NetworkPanel.instance();
|
225 | },
|
226 | });
|
227 |
|
228 | // A keybinding that is only active when the network panel is open and visible for
|
229 | // the user. It can hide the detailed network request information when the user has
|
230 | // clicked on a specific network request.
|
231 | // For more information about action extensions, please see the documentation in
|
232 | // `front_end/ui/ActionRegistration.ts`.
|
233 | UI.ActionRegistration.registerActionExtension({
|
234 | actionId: 'network.hide-request-details',
|
235 | category: UI.ActionRegistration.ActionCategory.NETWORK,
|
236 | title: i18nLazyString(UIStrings.hideRequestDetails),
|
237 | // If this function is not defined, this action is considered global.
|
238 | // For more detailed documentation about the definition and usage of `contextTypes`,
|
239 | // please see the `ActionRegistration` interface.
|
240 | contextTypes() {
|
241 | // This will return an array of relevant context types that should be loaded
|
242 | // and visible to the user for this action to be available in the command
|
243 | // menu or when the corresponding keybinding is invoked.
|
244 | return maybeRetrieveContextTypes(Network => [Network.NetworkPanel.NetworkPanel]);
|
245 | },
|
246 | // Just like the network panel, lazily load the action definition when requested by
|
247 | // the user.
|
248 | async loadActionDelegate() {
|
249 | const Network = await loadNetworkModule();
|
250 | return Network.NetworkPanel.ActionDelegate.instance();
|
251 | },
|
252 | bindings: [
|
253 | {
|
254 | shortcut: 'Esc',
|
255 | },
|
256 | ],
|
257 | });
|
258 | ```
|
259 |
|
260 | The ":meta" `devtools_entrypoint` is added as a dependency to all DevTools application entrypoints defined in [front_end/BUILD.gn](front_end/BUILD.gn).
|
261 |
|
262 | # Folder structure
|
263 |
|
264 | DevTools frontend is divided in multiple folders:
|
265 |
|
266 | - `core/` includes code that can be used by any other module.
|
267 | It typically includes utility functions as well as integration with backend.
|
268 | - `models/` includes business logic and handling of data received from the backend.
|
269 | - `panels/` includes high-level panels and top-level features.
|
270 | Each panel typically maps to a separate panel in DevTools, but some panels are integrated into others.
|
271 | - `ui/components/` includes reusable components that can be used to build multiple panels.
|
272 | - `ui/legacy/components/` includes legacy components.
|
273 | Please favor using `ui/components` wherever possible.
|
274 | - `entrypoints/` includes all entrypoints of DevTools, which can compose a variety of DevTools panels.
|
275 |
|
276 | In general, the following structure is applicable to dependencies between modules:
|
277 |
|
278 | ![An overview of allowed visibility rules of modules](./docs/images/module-visibility-rules.png)
|
279 |
|
280 | - `core/` can be imported by any module
|
281 | - `models/` can be imported by `panels/` and `entrypoints/`
|
282 | - `ui/` can be imported by `panels/` and `entrypoints/`
|
283 | - `panels/` can be imported by `entrypoints/`
|
284 |
|
285 | To enforce module imports adhere to these rules, actions specify their [GN `visibility` rules].
|
286 | An example bundle that is defined in `models/workspace_diff/BUILD.gn`:
|
287 |
|
288 | ```python
|
289 | devtools_entrypoint("bundle") {
|
290 | entrypoint = "workspace_diff.ts"
|
291 |
|
292 | deps = [ ":workspace_diff" ]
|
293 |
|
294 | visibility = [
|
295 | # Allow importing in this same module
|
296 | ":*",
|
297 | # Only these two panels are allowed to use `workspace_diff`
|
298 | "../../panels/changes/*",
|
299 | "../../panels/sources/*",
|
300 | ]
|
301 |
|
302 | # Used to allow overrides for downstream projects. Please see below for more information
|
303 | visibility += devtools_models_visibility
|
304 | }
|
305 | ```
|
306 |
|
307 | There are downstream projects that either have forked DevTools or build on top of DevTools.
|
308 | By defining visibility rules using relative paths, in downstream projects there would be no way to update the visibility to allow for additional modules to be importing a module.
|
309 | For example, if a downstream project defines a new panel and it would need to depend on `workspace_diff`, it would need to change the visibility definition of `workspace_diff`, which it can't.
|
310 |
|
311 | To allow for additional visibility rules to be defined, any target in DevTools has to allow for overrides.
|
312 | These overrides are typically defined in `visibility.gni` files.
|
313 | For example, `models/visibility.gni` defines the following:
|
314 |
|
315 | ```python
|
316 | declare_args() {
|
317 | devtools_models_visibility = []
|
318 | }
|
319 | ```
|
320 |
|
321 | By declaring it as an GN arg, downstream projects can override their GN arg in their `/.gn` file.
|
322 | For example, `/.gn` could declare the following:
|
323 |
|
324 | ```python
|
325 | default_args = {
|
326 | devtools_models_visibility = [
|
327 | "//front_end/my/new/panel/that/depends/on/workspace_diff/*",
|
328 | ]
|
329 | }
|
330 | ```
|
331 |
|
332 | Now, the new panel is allowed to add a dependency edge on `models/workspace_diff:bundle`.
|
333 |
|
334 | # DevTools GRD integration
|
335 |
|
336 | To bundle DevTools with Chromium, DevTools builds its GRD file that will be consumed by [GRIT].
|
337 | The GRD file lists all required files that should be loaded either in Debug or Release mode.
|
338 | All files that should be bundled are listed in `config/gni/devtools_grd_files.gni`.
|
339 | If a file should be present in debug and release mode, add the file to `grd_files_release_sources`.
|
340 | If a file should only be present in debug mode, add the file to `grd_files_debug_sources`.
|
341 |
|
342 | Note that `devtools_module` and `devtools_entrypoint` automatically take care of this for you.
|
343 | Any file included in `devtools_module` is only present in the Debug GRD file.
|
344 | Any file included as entrypoint of `devtools_entrypoint` is present in both the Release and Debug GRD file.
|
345 |
|
346 | There are additional actions that can add files to the GRD file, for example images or Markdown files.
|
347 | To allow for any action to define any relevant GRD file inclusions, DevTools uses [GN `metadata` definitions].
|
348 | For a custom action, specify the following:
|
349 |
|
350 | ```python
|
351 | node_action("generate_css_vars") {
|
352 | <...>
|
353 |
|
354 | # The output generated by the action. Not every file listed here is intended to be included
|
355 | # in the GRD bundle. For example, we don't want our TypeScript configuration files to be included.
|
356 | outputs = [
|
357 | "$target_gen_dir/Images.js",
|
358 | "$target_gen_dir/$target_name-tsconfig.json",
|
359 | ]
|
360 |
|
361 | data = [ "$target_gen_dir/Images.js" ]
|
362 |
|
363 | # Any `metadata` block can define a variable named `grd_files` which takes an `array` of
|
364 | # generated files. The location should be in the `gen/` directory, not the original source
|
365 | # location.
|
366 | #
|
367 | # In this case, we are listing both the generated `Images.js` file, as well as all image
|
368 | # files that DevTools has.
|
369 | metadata = {
|
370 | grd_files = data
|
371 | foreach(_image_file, devtools_image_files) {
|
372 | grd_files += [ "$target_gen_dir/$_image_file" ]
|
373 | }
|
374 | }
|
375 | }
|
376 | ```
|
377 |
|
378 | The GN metadata is traversed in `/BUILD.gn` and is written to a JSON file in `:input_grd_files`.
|
379 | The JSON file is compared to the contents of `:expected_grd_files` which takes the `grd_files_release_sources` and `grd_files_debug_sources` (only in Debug mode) files as input.
|
380 |
|
381 | If the collected input files matches the expected listed GRD files, the GRD files is written to a GRD file by `:generate_devtools_grd`.
|
382 | The GRD file is placed in `gen/third_party/devtools-frontend/src/front_end/` in Chromium.
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | [Web Platform Design Principles]: https://w3ctag.github.io/design-principles/
|
388 | [Put user needs first]: https://w3ctag.github.io/design-principles/#priority-of-constituencies
|
389 | [Prefer simple solutions]: https://w3ctag.github.io/design-principles/#simplicity
|
390 | [Load only what is necessary]: #load-only-what-is-necessary
|
391 | [Prefer web platform features whenever possible]: #prefer-web-platform-features-whenever-possible
|
392 | [Design with continuous deployment in mind]: #design-with-continuous-deployment-in-mind
|
393 |
|
394 |
|
395 | [dynamic imports]: https://v8.dev/features/dynamic-import
|
396 | [JavaScript modules]: https://v8.dev/features/modules
|
397 |
|
398 |
|
399 | [Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
|
400 | [ESLint]: https://eslint.org/
|
401 | [GN]: https://gn.googlesource.com/gn/+/main/
|
402 | [GN `visibility` rules]: https://gn.googlesource.com/gn/+/main/docs/reference.md#var_visibility
|
403 | [GN `metadata` definitions]: https://gn.googlesource.com/gn/+/main/docs/reference.md#var_metadata
|
404 | [GRIT]: https://www.chromium.org/developers/tools-we-use-in-chromium/grit/grit-users-guide
|
405 | [ndb]: https://github.com/GoogleChromeLabs/ndb
|
406 | [Ninja]: https://ninja-build.org/
|
407 | [TypeScript]: https://www.typescriptlang.org/
|