Merge "Modify trace redactor target to be included in module apex" into main
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index d53b0c3..a34ff08 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -36,7 +36,7 @@
from dataclasses import dataclass
-EXPECTED_ANY_COUNT = 52
+EXPECTED_ANY_COUNT = 51
EXPECTED_RUN_METRIC_COUNT = 4
ROOT_DIR = os.path.dirname(
diff --git a/src/base/time.cc b/src/base/time.cc
index e799542..ad971af 100644
--- a/src/base/time.cc
+++ b/src/base/time.cc
@@ -188,10 +188,15 @@
std::string GetTimeFmt(const std::string& fmt) {
time_t raw_time;
time(&raw_time);
- struct tm* local_tm;
- local_tm = localtime(&raw_time);
+ struct tm local_tm;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+ PERFETTO_CHECK(localtime_s(&local_tm, &raw_time) == 0);
+#else
+ tzset();
+ PERFETTO_CHECK(localtime_r(&raw_time, &local_tm) != nullptr);
+#endif
char buf[128];
- PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), local_tm) > 0);
+ PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), &local_tm) > 0);
return buf;
}
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
index b97e598..66f89fe 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
@@ -1 +1 @@
-ef239fe1e10e3b830f7adf9b5b8f96a52c69a50e6d879cfb5a873cd26caab769
\ No newline at end of file
+ae6623f3d00536a815ad76c40c5933845a436e6e8acee6349d0e925c06d0b6b0
\ No newline at end of file
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 7662755..c1045e2 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -214,8 +214,8 @@
return this.appCtx.currentTrace?.forPlugin(this.pluginId);
}
- scheduleFullRedraw(): void {
- raf.scheduleFullRedraw();
+ scheduleFullRedraw(force?: 'force'): void {
+ raf.scheduleFullRedraw(force);
}
get httpRpc() {
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
index fdf6ee6..ad0f482 100644
--- a/ui/src/core/command_manager.ts
+++ b/ui/src/core/command_manager.ts
@@ -15,6 +15,7 @@
import {FuzzyFinder, FuzzySegment} from '../base/fuzzy';
import {Registry} from '../base/registry';
import {Command, CommandManager} from '../public/command';
+import {raf} from './raf_scheduler';
export interface CommandWithMatchInfo extends Command {
segments: FuzzySegment[];
@@ -39,10 +40,11 @@
return this.registry.register(cmd);
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- runCommand(id: string, ...args: any[]): any {
+ runCommand(id: string, ...args: unknown[]): unknown {
const cmd = this.registry.get(id);
- return cmd.callback(...args);
+ const res = cmd.callback(...args);
+ Promise.resolve(res).finally(() => raf.scheduleFullRedraw('force'));
+ return res;
}
// Returns a list of commands that match the search term, along with a list
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts
index c39245f..41196c7 100644
--- a/ui/src/core/load_trace.ts
+++ b/ui/src/core/load_trace.ts
@@ -147,7 +147,7 @@
ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
});
}
- engine.onResponseReceived = () => raf.scheduleFullRedraw();
+ engine.onResponseReceived = () => raf.scheduleFullRedraw('force');
if (isMetatracingEnabled()) {
engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index b23379f..2935963 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -13,10 +13,19 @@
// limitations under the License.
import {PerfStats} from './perf_stats';
+import m from 'mithril';
+import {featureFlags} from './feature_flags';
export type AnimationCallback = (lastFrameMs: number) => void;
export type RedrawCallback = () => void;
+export const AUTOREDRAW_FLAG = featureFlags.register({
+ id: 'mithrilAutoredraw',
+ name: 'Enable Mithril autoredraw',
+ description: 'Turns calls to schedulefullRedraw() a no-op',
+ defaultValue: false,
+});
+
// This class orchestrates all RAFs in the UI. It ensures that there is only
// one animation frame handler overall and that callbacks are called in
// predictable order. There are two types of callbacks here:
@@ -34,12 +43,12 @@
// These happen at the end of full (DOM) animation frames.
private postRedrawCallbacks = new Array<RedrawCallback>();
- private syncDomRedrawFn: () => void = () => {};
private hasScheduledNextFrame = false;
private requestedFullRedraw = false;
private isRedrawing = false;
private _shutdown = false;
private recordPerfStats = false;
+ private mounts = new Map<Element, m.ComponentTypes>();
readonly perfStats = {
rafActions: new PerfStats(),
@@ -49,16 +58,23 @@
domRedraw: new PerfStats(),
};
- // Called by frontend/index.ts. syncDomRedrawFn is a function that invokes
- // m.render() of the root UiMain component.
- initialize(syncDomRedrawFn: () => void) {
- this.syncDomRedrawFn = syncDomRedrawFn;
+ constructor() {
+ // Patch m.redraw() to our RAF full redraw.
+ const origSync = m.redraw.sync;
+ const redrawFn = () => this.scheduleFullRedraw('force');
+ redrawFn.sync = origSync;
+ m.redraw = redrawFn;
+
+ m.mount = this.mount.bind(this);
}
// Schedule re-rendering of virtual DOM and canvas.
// If a callback is passed it will be executed after the DOM redraw has
// completed.
- scheduleFullRedraw(cb?: RedrawCallback) {
+ scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
+ // If we are using autoredraw mode, make this function a no-op unless
+ // 'force' is passed.
+ if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
this.requestedFullRedraw = true;
cb && this.postRedrawCallbacks.push(cb);
this.maybeScheduleAnimationFrame(true);
@@ -88,6 +104,16 @@
};
}
+ mount(element: Element, component: m.ComponentTypes | null): void {
+ const mounts = this.mounts;
+ if (component === null) {
+ mounts.delete(element);
+ } else {
+ mounts.set(element, component);
+ }
+ this.syncDomRedrawMountEntry(element, component);
+ }
+
shutdown() {
this._shutdown = true;
}
@@ -103,12 +129,37 @@
private syncDomRedraw() {
const redrawStart = performance.now();
- this.syncDomRedrawFn();
+
+ for (const [element, component] of this.mounts.entries()) {
+ this.syncDomRedrawMountEntry(element, component);
+ }
+
if (this.recordPerfStats) {
this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
}
}
+ private syncDomRedrawMountEntry(
+ element: Element,
+ component: m.ComponentTypes | null,
+ ) {
+ // Mithril's render() function takes a third argument which tells us if a
+ // further redraw is needed (e.g. due to managed event handler). This allows
+ // us to implement auto-redraw. The redraw argument is documented in the
+ // official Mithril docs but is just not part of the @types/mithril package.
+ const mithrilRender = m.render as (
+ el: Element,
+ vnodes: m.Children,
+ redraw?: () => void,
+ ) => void;
+
+ mithrilRender(
+ element,
+ component !== null ? m(component) : null,
+ AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
+ );
+ }
+
private syncCanvasRedraw() {
const redrawStart = performance.now();
if (this.isRedrawing) return;
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 99d4157..3e27b07 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -242,7 +242,7 @@
this.uploadStatus = '';
const uploader = new GcsUploader(this.traceData, {
onProgress: () => {
- raf.scheduleFullRedraw();
+ raf.scheduleFullRedraw('force');
this.uploadStatus = uploader.getEtaString();
if (uploader.state === 'UPLOADED') {
this.traceState = 'UPLOADED';
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index 819f271..322748e 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -54,7 +54,7 @@
nativeKeyboardLayoutMap()
.then((keyMap: KeyboardLayoutMap) => {
this.keyMap = keyMap;
- AppImpl.instance.scheduleFullRedraw();
+ AppImpl.instance.scheduleFullRedraw('force');
})
.catch((e) => {
if (
@@ -69,7 +69,7 @@
// The alternative would be to show key mappings for all keyboard
// layouts which is not feasible.
this.keyMap = new EnglishQwertyKeyboardLayoutMap();
- AppImpl.instance.scheduleFullRedraw();
+ AppImpl.instance.scheduleFullRedraw('force');
} else {
// Something unexpected happened. Either the browser doesn't conform
// to the keyboard API spec, or the keyboard API spec has changed!
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 4c87e4d..600f08b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -63,7 +63,7 @@
});
function routeChange(route: Route) {
- raf.scheduleFullRedraw(() => {
+ raf.scheduleFullRedraw('force', () => {
if (route.fragment) {
// This needs to happen after the next redraw call. It's not enough
// to use setTimeout(..., 0); since that may occur before the
@@ -148,7 +148,7 @@
});
// Wire up raf for widgets.
- setScheduleFullRedraw(() => raf.scheduleFullRedraw());
+ setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force));
// Load the css. The load is asynchronous and the CSS is not ready by the time
// appendChild returns.
@@ -225,12 +225,8 @@
const router = new Router();
router.onRouteChanged = routeChange;
- raf.initialize(() =>
- m.render(
- document.body,
- m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
- ),
- );
+ // Mount the main mithril component. This also forces a sync render pass.
+ raf.mount(document.body, UiMain);
if (
(location.origin.startsWith('http://localhost:') ||
@@ -269,12 +265,6 @@
routeChange(route);
});
- // Force one initial render to get everything in place
- m.render(
- document.body,
- m(UiMain, AppImpl.instance.pages.renderPageForCurrentRoute(undefined)),
- );
-
// Initialize plugins, now that we are ready to go.
const pluginManager = AppImpl.instance.plugins;
CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p));
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 5f1e29a..c94ee3f 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -328,7 +328,13 @@
document.removeEventListener('mousedown', this.onMouseDown);
}
+ // This is defined as an arrow function to have a single handler that can be
+ // added/remove while keeping `this` bound.
private onMouseDown = (e: Event) => {
+ // We need to schedule a redraw manually as this event handler was added
+ // manually to the DOM and doesn't use Mithril's auto-redraw system.
+ raf.scheduleFullRedraw('force');
+
// Don't close if the click was within ourselves or our popup.
if (e.target instanceof Node) {
if (this.popupElement && this.popupElement.contains(e.target)) {
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 8ece2d3..9c03eb7 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -352,7 +352,9 @@
}
export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
- private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
+ private _redrawWhileAnimating = new Animation(() =>
+ raf.scheduleFullRedraw('force'),
+ );
private _asyncJobPending = new Set<string>();
private _sectionExpanded = new Map<string, boolean>();
@@ -523,7 +525,7 @@
raf.scheduleFullRedraw();
res.finally(() => {
this._asyncJobPending.delete(itemId);
- raf.scheduleFullRedraw();
+ raf.scheduleFullRedraw('force');
});
};
}
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 0662ef8..04aee7e 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -163,7 +163,7 @@
/* onDrag */ (_x, y) => {
const deltaYSinceDragStart = dragStartY - y;
this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
- raf.scheduleFullRedraw();
+ raf.scheduleFullRedraw('force');
},
/* onDragStarted */ (_x, y) => {
this.resizableHeight = this.height;
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index c67f5b7..b8d8dda 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -52,9 +52,9 @@
// This wrapper creates a new instance of UiMainPerTrace for each new trace
// loaded (including the case of no trace at the beginning).
export class UiMain implements m.ClassComponent {
- view({children}: m.CVnode) {
+ view() {
const currentTraceId = AppImpl.instance.trace?.engine.engineId ?? '';
- return [m(UiMainPerTrace, {key: currentTraceId}, children)];
+ return [m(UiMainPerTrace, {key: currentTraceId})];
}
}
@@ -629,12 +629,13 @@
this.maybeFocusOmnibar();
}
- view({children}: m.Vnode): m.Children {
+ view(): m.Children {
+ const app = AppImpl.instance;
const hotkeys: HotkeyConfig[] = [];
- for (const {id, defaultHotkey} of AppImpl.instance.commands.commands) {
+ for (const {id, defaultHotkey} of app.commands.commands) {
if (defaultHotkey) {
hotkeys.push({
- callback: () => AppImpl.instance.commands.runCommand(id),
+ callback: () => app.commands.runCommand(id),
hotkey: defaultHotkey,
});
}
@@ -650,10 +651,10 @@
omnibox: this.renderOmnibox(),
trace: this.trace,
}),
- children,
+ app.pages.renderPageForCurrentRoute(app.trace),
m(CookieConsent),
maybeRenderFullscreenModalDialog(),
- AppImpl.instance.perfDebugging.renderPerfStats(),
+ app.perfDebugging.renderPerfStats(),
),
);
}
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
index 31ea7de..78c59c8 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
@@ -27,11 +27,14 @@
const MARGIN = 2;
const RECT_HEIGHT = 18;
+const RECT_WIDTH = 8;
const TRACK_HEIGHT = RECT_HEIGHT + 2 * MARGIN;
-export interface Data extends TrackData {
- timestamps: BigInt64Array;
- names: string[];
+interface Data extends TrackData {
+ events: Array<{
+ timestamp: time;
+ color: string;
+ }>;
}
export interface Config {
@@ -92,21 +95,22 @@
order by tsQuant limit ${LIMIT};`);
const rowCount = queryRes.numRows();
- const result: Data = {
+
+ const it = queryRes.iter({tsQuant: LONG, name: STR});
+ const events = [];
+ for (let row = 0; it.valid(); it.next(), row++) {
+ events.push({
+ timestamp: Time.fromRaw(it.tsQuant),
+ color: colorForFtrace(it.name).base.cssString,
+ });
+ }
+ return {
start,
end,
resolution,
length: rowCount,
- timestamps: new BigInt64Array(rowCount),
- names: [],
+ events,
};
-
- const it = queryRes.iter({tsQuant: LONG, name: STR});
- for (let row = 0; it.valid(); it.next(), row++) {
- result.timestamps[row] = it.tsQuant;
- result.names[row] = it.name;
- }
- return result;
}
render({ctx, size, timescale}: TrackRenderContext): void {
@@ -125,21 +129,10 @@
dataStartPx,
dataEndPx,
);
-
- const diamondSideLen = RECT_HEIGHT / Math.sqrt(2);
-
- for (let i = 0; i < data.timestamps.length; i++) {
- const name = data.names[i];
- ctx.fillStyle = colorForFtrace(name).base.cssString;
- const timestamp = Time.fromRaw(data.timestamps[i]);
- const xPos = Math.floor(timescale.timeToPx(timestamp));
-
- // Draw a diamond over the event
- ctx.save();
- ctx.translate(xPos, MARGIN);
- ctx.rotate(Math.PI / 4);
- ctx.fillRect(0, 0, diamondSideLen, diamondSideLen);
- ctx.restore();
+ for (const e of data.events) {
+ ctx.fillStyle = e.color;
+ const xPos = Math.floor(timescale.timeToPx(e.timestamp));
+ ctx.fillRect(xPos - RECT_WIDTH / 2, MARGIN, RECT_WIDTH, RECT_HEIGHT);
}
}
}
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index aef7a76..37206a3 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -272,6 +272,7 @@
private onmessage(msg: MessageEvent) {
if (this._ctx === undefined) return; // Trace unloaded
if (!('perfettoSync' in msg.data)) return;
+ this._ctx.scheduleFullRedraw('force');
const msgData = msg.data as SyncMessage;
const sync = msgData.perfettoSync;
switch (sync.cmd) {
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 327a179..a5ff6d7 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -685,6 +685,7 @@
icon: arg(icon, 'send'),
rightIcon: arg(rightIcon, 'arrow_forward'),
label: arg(label, 'Button', ''),
+ onclick: () => alert('button pressed'),
...rest,
}),
initialOpts: {
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 50def57..0c8321b 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -54,7 +54,7 @@
// TODO(primiano): this should be needed in extremely rare cases. We should
// probably switch to mithril auto-redraw at some point.
- scheduleFullRedraw(): void;
+ scheduleFullRedraw(force?: 'force'): void;
/**
* Navigate to a new page.
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 58ea153..1187be0 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -21,6 +21,7 @@
import {assertExists} from '../base/logging';
import {DragGestureHandler} from '../base/drag_gesture_handler';
import {DisposableStack} from '../base/disposable_stack';
+import {scheduleFullRedraw} from './raf';
export interface EditorAttrs {
// Initial state for the editor.
@@ -64,6 +65,7 @@
text = selectedText;
}
onExecute(text);
+ scheduleFullRedraw('force');
return true;
},
});
@@ -75,6 +77,7 @@
view.update([tr]);
const text = view.state.doc.toString();
onUpdate(text);
+ scheduleFullRedraw('force');
};
}
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index f4d702a..767683e 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -14,6 +14,7 @@
import m from 'mithril';
import {checkHotkey, Hotkey} from '../base/hotkeys';
+import {scheduleFullRedraw} from './raf';
export interface HotkeyConfig {
hotkey: Hotkey;
@@ -58,6 +59,7 @@
if (checkHotkey(hotkey, e)) {
e.preventDefault();
callback();
+ scheduleFullRedraw('force');
}
});
}
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index e32f329..c07e6fe 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -14,8 +14,8 @@
import m from 'mithril';
import {defer} from '../base/deferred';
-import {scheduleFullRedraw} from './raf';
import {Icon} from './icon';
+import {scheduleFullRedraw} from './raf';
// This module deals with modal dialogs. Unlike most components, here we want to
// render the DOM elements outside of the corresponding vdom tree. For instance
@@ -79,7 +79,10 @@
export class Modal implements m.ClassComponent<ModalAttrs> {
onbeforeremove(vnode: m.VnodeDOM<ModalAttrs>) {
const removePromise = defer<void>();
- vnode.dom.addEventListener('animationend', () => removePromise.resolve());
+ vnode.dom.addEventListener('animationend', () => {
+ scheduleFullRedraw('force');
+ removePromise.resolve();
+ });
vnode.dom.classList.add('modal-fadeout');
// Retuning `removePromise` will cause Mithril to defer the actual component
@@ -94,7 +97,6 @@
// in turn will: (1) call the user's original attrs.onClose; (2) resolve
// the promise returned by showModal().
vnode.attrs.onClose();
- scheduleFullRedraw();
}
}
@@ -223,7 +225,7 @@
},
};
currentModal = attrs;
- scheduleFullRedraw();
+ redrawModal();
return returnedClosePromise;
}
@@ -232,7 +234,7 @@
// evident why a redraw is requested.
export function redrawModal() {
if (currentModal !== undefined) {
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
}
@@ -251,7 +253,7 @@
return;
}
currentModal = undefined;
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
export function getCurrentModalKey(): string | undefined {
diff --git a/ui/src/widgets/popup.ts b/ui/src/widgets/popup.ts
index ed16695..ac8b563 100644
--- a/ui/src/widgets/popup.ts
+++ b/ui/src/widgets/popup.ts
@@ -352,13 +352,13 @@
if (this.isOpen) {
this.isOpen = false;
this.onChange(this.isOpen);
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
}
private togglePopup() {
this.isOpen = !this.isOpen;
this.onChange(this.isOpen);
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
}
diff --git a/ui/src/widgets/portal.ts b/ui/src/widgets/portal.ts
index 734fee6..91c3608 100644
--- a/ui/src/widgets/portal.ts
+++ b/ui/src/widgets/portal.ts
@@ -46,13 +46,22 @@
export class Portal implements m.ClassComponent<PortalAttrs> {
private portalElement?: HTMLElement;
private containerElement?: Element;
+ private contentComponent: m.Component;
+
+ constructor({children}: m.CVnode<PortalAttrs>) {
+ // Create a temporary component that we can mount in oncreate, and unmount
+ // in onremove, but inject the new portal content (children) into it each
+ // render cycle. This is initialized here rather than in oncreate to avoid
+ // having to make it optional or use assertExists().
+ this.contentComponent = {view: () => children};
+ }
view() {
// Dummy element renders nothing but permits DOM access in lifecycle hooks.
return m('span', {style: {display: 'none'}});
}
- oncreate({attrs, children, dom}: m.VnodeDOM<PortalAttrs, this>) {
+ oncreate({attrs, dom}: m.CVnodeDOM<PortalAttrs>) {
const {
onContentMount = () => {},
onBeforeContentMount = (): MountOptions => ({}),
@@ -65,16 +74,21 @@
container.appendChild(this.portalElement);
this.applyPortalProps(attrs);
- m.render(this.portalElement, children);
+ m.mount(this.portalElement, this.contentComponent);
onContentMount(this.portalElement);
}
- onupdate({attrs, children}: m.VnodeDOM<PortalAttrs, this>) {
+ onbeforeupdate({children}: m.CVnode<PortalAttrs>) {
+ // Update the mounted content's view function to return the latest portal
+ // content passed in via children, without changing the component itself.
+ this.contentComponent.view = () => children;
+ }
+
+ onupdate({attrs}: m.CVnodeDOM<PortalAttrs>) {
const {onContentUpdate = () => {}} = attrs;
if (this.portalElement) {
this.applyPortalProps(attrs);
- m.render(this.portalElement, children);
onContentUpdate(this.portalElement);
}
}
@@ -86,14 +100,14 @@
}
}
- onremove({attrs}: m.VnodeDOM<PortalAttrs, this>) {
+ onremove({attrs}: m.CVnodeDOM<PortalAttrs>) {
const {onContentUnmount = () => {}} = attrs;
const container = this.containerElement ?? document.body;
if (this.portalElement) {
if (container.contains(this.portalElement)) {
onContentUnmount(this.portalElement);
// Rendering null ensures previous vnodes are removed properly.
- m.render(this.portalElement, null);
+ m.mount(this.portalElement, null);
container.removeChild(this.portalElement);
}
}
diff --git a/ui/src/widgets/raf.ts b/ui/src/widgets/raf.ts
index 20afb61..dc0d3ab 100644
--- a/ui/src/widgets/raf.ts
+++ b/ui/src/widgets/raf.ts
@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-let FULL_REDRAW_FUNCTION = () => {};
+let FULL_REDRAW_FUNCTION = (_force?: 'force') => {};
export function setScheduleFullRedraw(func: () => void) {
FULL_REDRAW_FUNCTION = func;
}
-export function scheduleFullRedraw() {
- FULL_REDRAW_FUNCTION();
+export function scheduleFullRedraw(force?: 'force') {
+ FULL_REDRAW_FUNCTION(force);
}
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 7cbf533..1a8cb43 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -228,7 +228,7 @@
}
this._status = Status.Done;
this.pending = undefined;
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
private handleError(pending: Promise<vega.View>, err: unknown) {
@@ -242,7 +242,7 @@
private setError(err: unknown) {
this._status = Status.Error;
this._error = getErrorMessage(err);
- scheduleFullRedraw();
+ scheduleFullRedraw('force');
}
[Symbol.dispose]() {
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 475c360..6172c94 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -14,6 +14,7 @@
import {DisposableStack} from '../base/disposable_stack';
import {Bounds2D, Rect2D} from '../base/geom';
+import {scheduleFullRedraw} from './raf';
export interface VirtualScrollHelperOpts {
overdrawPx: number;
@@ -46,6 +47,7 @@
this._data.forEach((data) =>
recalculatePuckRect(sliderElement, containerElement, data),
);
+ scheduleFullRedraw('force');
};
containerElement.addEventListener('scroll', recalculateRects, {