Skip to content

Commit

Permalink
perf(platform): optimize focus tracking and mouse event dispatching
Browse files Browse the repository at this point in the history
Optimizations include:
- Reducing the number of events for the platform to track focus across microfrontends.
- Dispatching of synthetic mouse events only from inactive microfrontends.
- Delivery of synthetic mouse events to the active microfrontend only.
- Disabling event listeners that are not needed in activators.
- Listening to keyboard events only in contexts where keystrokes are registered.

closes #172
  • Loading branch information
danielwiehl authored and Marcarrian committed Nov 23, 2022
1 parent e5dc6c2 commit daff4f0
Show file tree
Hide file tree
Showing 13 changed files with 608 additions and 121 deletions.
10 changes: 5 additions & 5 deletions apps/microfrontend-platform-testing-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import {Component, HostListener, NgZone, OnDestroy} from '@angular/core';
import {ContextService, MicrofrontendPlatform, OUTLET_CONTEXT, OutletContext} from '@scion/microfrontend-platform';
import {Beans} from '@scion/toolkit/bean-manager';
import {fromEvent, merge, Subject} from 'rxjs';
import {fromEvent, merge, Subject, withLatestFrom} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {subscribeInside} from '@scion/toolkit/operators';

Expand All @@ -21,7 +21,7 @@ import {subscribeInside} from '@scion/toolkit/operators';
export class AppComponent implements OnDestroy {

private _destroy$ = new Subject<void>();
private _outletContext: Promise<OutletContext>;
private _outletContext: Promise<OutletContext | null>;

constructor(private _zone: NgZone) {
this._outletContext = Beans.get(ContextService).lookup<OutletContext>(OUTLET_CONTEXT);
Expand All @@ -45,13 +45,13 @@ export class AppComponent implements OnDestroy {
private installPropagatedKeyboardEventLogger(): void {
merge(fromEvent<KeyboardEvent>(document, 'keydown'), fromEvent<KeyboardEvent>(document, 'keyup'))
.pipe(
withLatestFrom(this._outletContext),
subscribeInside(fn => this._zone.runOutsideAngular(fn)),
takeUntil(this._destroy$),
)
.subscribe(event => async () => {
.subscribe(([event, outletContext]: [KeyboardEvent, OutletContext | null]) => {
if (!event.isTrusted && (event.target as Element).tagName === 'SCI-ROUTER-OUTLET') {
const outletContextName = (await this._outletContext)?.name ?? 'n/a';
console.debug(`[AppComponent::document:on${event.type}] [SYNTHETIC] [outletContext=${outletContextName}, key='${event.key}', control=${event.ctrlKey}, shift=${event.shiftKey}, alt=${event.altKey}, meta=${event.metaKey}]`);
console.debug(`[AppComponent::document:on${event.type}] [SYNTHETIC] [outletContext=${outletContext?.name ?? 'n/a'}, key='${event.key}', control=${event.ctrlKey}, shift=${event.shiftKey}, alt=${event.altKey}, meta=${event.metaKey}]`);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="e2e-element element" tabindex="0" (mousedown)="!preventdefault_on_mousedown.isChecked">Element</div>

<label>
<sci-checkbox class="e2e-prevent-default-on-mousedown" #preventdefault_on_mousedown></sci-checkbox>
<span>PreventDefault on Mousedown</span>
</label>

<section class="dispatched-mouse-events e2e-dispatched-mouse-events">
<header>Received Dispatched Mouse Events</header>
<sci-viewport>
<div *ngFor="let dispatchedEvent of dispatchedEvents" class="event e2e-event">
<span>{{dispatchedEvent.timestamp | date:'hh:mm:ss:SSS'}}</span><span class="e2e-type">{{dispatchedEvent.type}}</span>
</div>
</sci-viewport>
<label>
<sci-checkbox [formControl]="followTailFormControl"></sci-checkbox>
<span>Follow tail</span>
</label>
<button (click)="onClearDispatchedEvent()" class="e2e-clear">Clear</button>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
:host {
display: flex;
flex-direction: column;
gap: 1em;

> div.element {
flex: none;
border: 1px dashed var(--sci-color-P400);
color: var(--sci-color-P500);
padding: 1em;
user-select: none;
text-align: center;
}

> section.dispatched-mouse-events {
flex: auto;
display: flex;
flex-direction: column;
gap: .5em;
margin-top: .5em;

> header {
flex: none;
font-weight: bold;
}

> sci-viewport {
flex: 1 1 0;
border: 1px solid var(--sci-color-P400);
font-family: monospace;

&::part(content) {
display: flex;
flex-direction: column;
gap: .25em;
padding: .5em;
}

div.event {
display: flex;
gap: 1em;
}
}
}

label {
display: flex;
gap: .75em;
align-items: center;
user-select: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2022 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {ChangeDetectorRef, Component, OnDestroy, ViewChild} from '@angular/core';
import {takeUntil} from 'rxjs/operators';
import {fromEvent, merge, Subject} from 'rxjs';
import {CommonModule} from '@angular/common';
import {SciViewportComponent, SciViewportModule} from '@scion/components/viewport';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {SciCheckboxModule} from '@scion/components.internal/checkbox';

@Component({
selector: 'app-mouse-event-dispatch-test-page',
templateUrl: './mouse-event-dispatch-test-page.component.html',
styleUrls: ['./mouse-event-dispatch-test-page.component.scss'],
standalone: true,
imports: [
CommonModule,
SciViewportModule,
SciCheckboxModule,
ReactiveFormsModule,
],
})
export class MouseEventDispatchTestPageComponent implements OnDestroy {

private _destroy$ = new Subject<void>();

public dispatchedEvents = new Array<DispatchedEvent>();
public followTailFormControl = new FormControl<boolean>(true);

@ViewChild(SciViewportComponent)
private _viewport: SciViewportComponent;

constructor(private _cd: ChangeDetectorRef) {
merge(fromEvent(document, 'sci-mousemove'), fromEvent(document, 'sci-mouseup'))
.pipe(takeUntil(this._destroy$))
.subscribe(event => {
this.dispatchedEvents.push({type: event.type, timestamp: Date.now()});
if (this.followTailFormControl.value) {
this._cd.detectChanges();
this._viewport.scrollTop = this._viewport.scrollHeight;
}
});
}

public onClearDispatchedEvent(): void {
this.dispatchedEvents.length = 0;
}

public ngOnDestroy(): void {
this._destroy$.next();
}
}

export interface DispatchedEvent {
type: string;
timestamp: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const routes: Routes = [
data: {pageTitle: 'Test page that clears an outlet and sends a message'},
loadComponent: (): any => import('./clear-outlet-then-send-message-test-page/clear-outlet-then-send-message-test-page.component').then(m => m.ClearOutletThenSendMessageTestPageComponent),
},
{
path: 'mouse-event-dispatch-test-page',
data: {pageTitle: 'Test page to test mouse event dispatching'},
loadComponent: (): any => import('./mouse-event-dispatch-test-page/mouse-event-dispatch-test-page.component').then(m => m.MouseEventDispatchTestPageComponent),
},
{
path: 'angular-zone-test-page',
data: {pageTitle: 'Test page to test NgZone Synchronization'},
Expand Down
Loading

0 comments on commit daff4f0

Please sign in to comment.