From 6145e89d47c0eb989ea4964c321f482fcee5ad70 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 16 Apr 2026 11:15:43 -0300 Subject: [PATCH 1/3] ref(trace): Expose scrollbar_width and re-dispatch on change Make scrollbar_width public so external listeners can subtract it from container width calculations. Re-dispatch 'set container physical space' whenever the scrollbar width is updated, triggering downstream listeners to recompute trace_physical_space with the correct narrowed width. Co-Authored-By: Claude Sonnet 4.6 --- .../traceRenderers/virtualizedViewManager.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index a5e6c619abb71f..59460ddd557ef1 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -102,7 +102,7 @@ export class VirtualizedViewManager { row_depth_padding = 22; - private scrollbar_width = 0; + scrollbar_width = 0; // the transformation matrix that is used to render scaled elements to the DOM private span_to_px: mat3 = mat3.create(); private readonly ROW_PADDING_PX = 16; @@ -259,6 +259,20 @@ export class VirtualizedViewManager { return; } this.scrollbar_width = width; + + // Re-dispatch the container physical space so that the trace_physical_space + // is recomputed accounting for the new scrollbar width. Without this, bars + // are positioned relative to a wider space than the actual column, shifting + // them to the right. + if (this.container) { + const rect = this.container.getBoundingClientRect(); + this.scheduler.dispatch('set container physical space', [ + 0, + 0, + rect.width, + rect.height, + ]); + } } registerContainerRef(container: HTMLElement | null) { From 960f64a3338e0fbacc75c5b85fc31ba194c20f66 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 16 Apr 2026 11:16:27 -0300 Subject: [PATCH 2/3] fix(trace): Subtract scrollbar width from trace physical space calculations When a vertical scrollbar is visible, the scroll container rows are narrower than the outer container measured by ResizeObserver. Without accounting for the scrollbar width, trace_physical_space is computed against a wider area than the actual renderable column, causing span bars to render shifted to the right. Subtract scrollbar_width from the container width in both the onPhysicalSpaceChange listener (useTraceSpaceListeners) and the column layout handler (virtualizedViewManager) so the physical space always matches the visible column width. Co-Authored-By: Claude Sonnet 4.6 --- .../traceRenderers/virtualizedViewManager.tsx | 2 +- .../performance/newTraceDetails/useTraceSpaceListeners.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index 59460ddd557ef1..8162bf5914b605 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -240,7 +240,7 @@ export class VirtualizedViewManager { } this.view.trace_physical_space.width = - span_list * this.view.trace_container_physical_space.width; + span_list * (this.view.trace_container_physical_space.width - this.scrollbar_width); this.scheduler.dispatch('set trace view', { x: this.view.trace_view.x, diff --git a/static/app/views/performance/newTraceDetails/useTraceSpaceListeners.tsx b/static/app/views/performance/newTraceDetails/useTraceSpaceListeners.tsx index 32b6774fa95de7..8944a25e88d07d 100644 --- a/static/app/views/performance/newTraceDetails/useTraceSpaceListeners.tsx +++ b/static/app/views/performance/newTraceDetails/useTraceSpaceListeners.tsx @@ -24,10 +24,15 @@ export function useTraceSpaceListeners(props: { const onPhysicalSpaceChange: TraceEvents['set container physical space'] = container => { + // Subtract the scrollbar width from the container width so that the + // physical space matches the actual renderable column width. When a + // vertical scrollbar is visible, the rows inside the scroll container + // are narrower than the outer container measured by ResizeObserver. + const adjustedWidth = container[2] - props.viewManager.scrollbar_width; props.view.setTracePhysicalSpace(container, [ 0, 0, - container[2] * props.viewManager.columns.span_list.width, + adjustedWidth * props.viewManager.columns.span_list.width, container[3], ]); }; From 4e60fd3e3d4b54a36c342be1595583b97be25eb4 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 16 Apr 2026 12:01:11 -0300 Subject: [PATCH 3/3] fix(trace): Use content box for container physical space on scrollbar change When the scrollbar width changes, re-dispatch the container physical space using the content box (excluding padding and border) instead of the full bounding client rect. This matches the box model that ResizeObserver uses for contentRect, preventing bars from being positioned relative to a wider space than the actual content area. Co-Authored-By: Claude Opus 4.6 --- .../virtualizedViewManager.spec.tsx | 44 +++++++++++++++++++ .../traceRenderers/virtualizedViewManager.tsx | 44 ++++++++++++++----- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.spec.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.spec.tsx index deb9627f0a066a..f777c46ade9202 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.spec.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.spec.tsx @@ -41,6 +41,50 @@ describe('VirtualizedViewManger', () => { expect(manager.view.trace_physical_space.serialize()).toEqual([0, 0, 500, 1]); }); + it('re-dispatches the container content box when scrollbar width changes', () => { + const scheduler = new TraceScheduler(); + const manager = new VirtualizedViewManager( + { + list: {width: 0.5}, + span_list: {width: 0.5}, + }, + scheduler, + new TraceView(), + ThemeFixture() + ); + + manager.view.setTracePhysicalSpace([0, 0, 500, 200], [0, 0, 250, 200]); + + const container = document.createElement('div'); + container.style.paddingTop = '38px'; + container.style.paddingLeft = '10px'; + container.style.paddingRight = '10px'; + manager.container = container; + + jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + top: 0, + left: 0, + bottom: 238, + right: 520, + width: 520, + height: 238, + toJSON: () => ({}), + } as DOMRect); + + let dispatchedContainerPhysicalSpace: [number, number, number, number] | null = null; + scheduler.on('set container physical space', containerPhysicalSpace => { + dispatchedContainerPhysicalSpace = containerPhysicalSpace; + }); + + manager.onScrollbarWidthChange(14); + + // 520 - 10 (paddingLeft) - 10 (paddingRight) = 500 + // 238 - 38 (paddingTop) = 200 + expect(dispatchedContainerPhysicalSpace).toEqual([0, 0, 500, 200]); + }); + describe('computeSpanCSSMatrixTransform', () => { it('enforces min scaling', () => { const manager = new VirtualizedViewManager( diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index 8162bf5914b605..76145ab9095523 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -260,19 +260,39 @@ export class VirtualizedViewManager { } this.scrollbar_width = width; - // Re-dispatch the container physical space so that the trace_physical_space - // is recomputed accounting for the new scrollbar width. Without this, bars - // are positioned relative to a wider space than the actual column, shifting - // them to the right. - if (this.container) { - const rect = this.container.getBoundingClientRect(); - this.scheduler.dispatch('set container physical space', [ - 0, - 0, - rect.width, - rect.height, - ]); + // Re-dispatch the container content box so that the trace_physical_space is + // recomputed accounting for the new scrollbar width using the same box + // model as ResizeObserver's contentRect. + const containerPhysicalSpace = this.getContainerContentPhysicalSpace(); + if (containerPhysicalSpace) { + this.scheduler.dispatch('set container physical space', containerPhysicalSpace); + } + } + + private getContainerContentPhysicalSpace(): + | [x: number, y: number, width: number, height: number] + | null { + if (!this.container) { + return null; + } + + const rect = this.container.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + return null; } + + const getBoxSize = (value: string) => Number.parseFloat(value) || 0; + const styles = window.getComputedStyle(this.container); + const paddingX = getBoxSize(styles.paddingLeft) + getBoxSize(styles.paddingRight); + const paddingY = getBoxSize(styles.paddingTop) + getBoxSize(styles.paddingBottom); + const borderX = + getBoxSize(styles.borderLeftWidth) + getBoxSize(styles.borderRightWidth); + const borderY = + getBoxSize(styles.borderTopWidth) + getBoxSize(styles.borderBottomWidth); + const width = Math.max(rect.width - paddingX - borderX, 0); + const height = Math.max(rect.height - paddingY - borderY, 0); + + return [0, 0, width, height]; } registerContainerRef(container: HTMLElement | null) {