Skip to content

refactor(devtools): improve profiler recorder controls #62010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
refactor(devtools): improve profiler recorder controls
Reorganize the template and reduce the vertical space taken by the controls.
  • Loading branch information
hawkgs committed Jun 11, 2025
commit da17039fbf762cb751912c3ac118ba6998eb7614
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ng_project(
"//:node_modules/@angular/material",
"//:node_modules/rxjs",
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/timeline:timeline_rjs",
"//devtools/projects/ng-devtools/src/lib/shared/button:button_rjs",
"//devtools/projects/protocol:protocol_rjs",
],
)
Original file line number Diff line number Diff line change
@@ -1,65 +1,66 @@
<div class="profiler-wrapper">
<mat-card>
@if (state() === 'idle') {
<div class="recorder-controls">
@switch (state()) {
@case ('idle') {
<button
mat-icon-button
color="primary"
ng-button
btnType="icon"
(click)="startRecording()"
class="profiler-control start-recording-button"
matTooltip="Start recording"
aria-label="Start recording"
>
<mat-icon> circle </mat-icon>
</button>
} @else if (state() === 'recording') {

<p class="instructions">
<span>
Click the record button to start a new recording, or upload a JSON file containing
profiler data.
</span>
<br />
<input
type="file"
(change)="importProfilerResults($event)"
placeholder="Upload file"
accept=".json"
/>
</p>
}
@case ('recording') {
<button
mat-icon-button
ng-button
btnType="icon"
(click)="stopRecording()"
class="profiler-control recording-button"
class="recording-button"
matTooltip="Stop recording"
aria-label="Stop recording"
>
<mat-icon> circle </mat-icon>
</button>
} @else if (state() === 'visualizing') {

<p class="instructions">
Interact to preview change detection. Clicking stop ends this Profiler recording.
</p>
}
@case ('visualizing') {
<button
mat-icon-button
color="primary"
ng-button
btnType="icon"
(click)="discardRecording()"
class="profiler-control discard-button"
matTooltip="Clear recording"
aria-label="Clear recording"
>
<mat-icon> not_interested </mat-icon>
</button>

<p class="instructions">
Click Save Profile to save your recording or click refresh to clear the current recording.
</p>
}
<p class="instructions" [class.hidden]="state() !== 'idle'">
<span>
Click the record button to start a new recording, or upload a JSON file containing profiler
data.
</span>
<br />
<span>
<input
type="file"
(change)="importProfilerResults($event)"
placeholder="Upload file"
accept=".json"
/>
</span>
</p>
<p class="instructions" [class.hidden]="state() !== 'recording'">
Interact to preview change detection. Clicking stop ends this Profiler recording.
</p>
<p class="instructions" [class.hidden]="state() !== 'visualizing'">
Click Save Profile to save your recording or click refresh to clear the current recording.
</p>
</mat-card>
<div id="profiler-content-wrapper">
@if (state() !== 'idle') {
<div class="visualization">
<ng-recording-timeline [stream]="stream" (exportProfile)="exportProfilerResults()" />
</div>
}
</div>
}
</div>

@if (state() !== 'idle') {
<div class="recording">
<ng-recording-timeline [stream]="stream" (exportProfile)="exportProfilerResults()" />
</div>
}
Original file line number Diff line number Diff line change
@@ -1,55 +1,47 @@
@use '../../../styles/typography';

:host,
.profiler-wrapper {
width: 100%;
height: calc(100% - 30px);

p {
margin: 0;
}
}

.mat-icon {
font-size: 18px;
top: 4px;
position: relative;
}

.profiler-control {
cursor: pointer;

&.recording-button {
color: var(--dynamic-red-01);
}
}

.instructions {
@extend %body-01;
:host {
display: flex;
flex-direction: column;
height: 100%;

&.hidden {
display: none;
.recorder-controls {
display: flex;
align-items: flex-start;
gap: 0.25rem;
border-bottom: 1px solid var(--color-separator);

button {
$icon-size: 18px;
margin: 0.625rem;
color: var(--secondary-contrast);
height: $icon-size;

&.recording-button {
color: var(--dynamic-red-01);
}

mat-icon {
width: $icon-size;
height: $icon-size;
font-size: $icon-size;
}
}

.instructions {
@extend %body-01;
padding-block: 0.7rem;
margin: 0;

input {
margin-top: 0.5rem;
cursor: pointer;
}
}
}

input,
span {
margin-top: 5px;
cursor: pointer;
.recording {
flex: 1;
overflow: hidden;
}
}

#profiler-content-wrapper {
margin: 0;
height: calc(100% - 30px);
width: 100%;
}

.visualization {
margin: 0;
height: 100%;
}

.mdc-card {
flex-direction: row;
align-items: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@

import {Component, inject, signal} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {Events, MessageBus, ProfilerFrame} from '../../../../../protocol';
import {Subject} from 'rxjs';

import {FileApiService} from './file-api-service';
import {ProfilerImportDialogComponent} from './profiler-import-dialog.component';
import {TimelineComponent} from './timeline/timeline.component';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {MatIconButton} from '@angular/material/button';
import {MatCard} from '@angular/material/card';
import {ButtonComponent} from '../../shared/button/button.component';

type State = 'idle' | 'recording' | 'visualizing';

Expand All @@ -28,7 +27,7 @@ const PROFILER_VERSION = 1;
selector: 'ng-profiler',
templateUrl: './profiler.component.html',
styleUrls: ['./profiler.component.scss'],
imports: [MatCard, MatIconButton, MatTooltip, MatIcon, TimelineComponent],
imports: [MatTooltip, MatIcon, TimelineComponent, ButtonComponent],
})
export class ProfilerComponent {
readonly state = signal<State>('idle');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
display: block;
overflow: auto;
height: 100%;
padding-bottom: 1rem;

::ng-deep {
.as-split-gutter-icon {
display: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@
background: var(--dynamic-blue-02);
color: var(--septenary-contrast);
}

&.type-icon {
background: transparent;
border: none;
padding: 0;

&.size-compact {
padding: 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {ChangeDetectionStrategy, Component, input} from '@angular/core';

type ButtonType = 'primary'; // To be extended in future
type ButtonType = 'primary' | 'icon';
type ButtonSize = 'standard' | 'compact';

@Component({
Expand All @@ -19,6 +19,7 @@ type ButtonSize = 'standard' | 'compact';
host: {
class: 'ng-button',
'[class.type-primary]': `btnType() === 'primary'`,
'[class.type-icon]': `btnType() === 'icon'`,
'[class.size-compact]': `size() === 'compact'`,
},
})
Expand Down
4 changes: 4 additions & 0 deletions devtools/projects/ng-devtools/src/styles/_global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,7 @@ mat-form-field {
);
}
}

[hidden] {
display: none !important;
Comment on lines +152 to +153
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add this global style in this PR ? I doesn't look like the PR is introducing its usage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Since tabs rely now on [hidden], I had to add this because changing the display property of a tab component (i.e. :host) overrides the default display: none by [hidden].

}
Loading