Add some quick-and-dirty debug info for TileStore (#2887)

* Add some quick-and-dirty debug info for TileStore

I'm still in need of more detailed data in order to understand why big layout shifts happen in large calls. This adds a developer option to enable logging and a visual indicator for the state of the TileStore. The indicator should be useful for matching up the behavior I'm seeing in my recordings with the right timestamps.

* Reduce performance impact of checking for whether debug mode is enabled

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
Robin
2024-12-11 05:23:42 -05:00
committed by GitHub
parent 54149a496c
commit b834d8f679
6 changed files with 91 additions and 6 deletions

View File

@@ -70,6 +70,7 @@
}, },
"developer_mode": { "developer_mode": {
"crypto_version": "Crypto version: {{version}}", "crypto_version": "Crypto version: {{version}}",
"debug_tile_layout_label": "Debug tile layout",
"device_id": "Device ID: {{id}}", "device_id": "Device ID: {{id}}",
"duplicate_tiles_label": "Number of additional tile copies per participant", "duplicate_tiles_label": "Number of additional tile copies per participant",
"hostname": "Hostname: {{hostname}}", "hostname": "Hostname: {{hostname}}",

View File

@@ -88,6 +88,10 @@ import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
import { useSwitchCamera } from "./useSwitchCamera"; import { useSwitchCamera } from "./useSwitchCamera";
import { ReactionsOverlay } from "./ReactionsOverlay"; import { ReactionsOverlay } from "./ReactionsOverlay";
import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
import {
debugTileLayout as debugTileLayoutSetting,
useSetting,
} from "../settings/settings";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -223,6 +227,8 @@ export const InCallView: FC<InCallViewProps> = ({
const windowMode = useObservableEagerState(vm.windowMode); const windowMode = useObservableEagerState(vm.windowMode);
const layout = useObservableEagerState(vm.layout); const layout = useObservableEagerState(vm.layout);
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration);
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
const gridMode = useObservableEagerState(vm.gridMode); const gridMode = useObservableEagerState(vm.gridMode);
const showHeader = useObservableEagerState(vm.showHeader); const showHeader = useObservableEagerState(vm.showHeader);
const showFooter = useObservableEagerState(vm.showFooter); const showFooter = useObservableEagerState(vm.showFooter);
@@ -585,6 +591,10 @@ export const InCallView: FC<InCallViewProps> = ({
height={11} height={11}
aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"} aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"}
/> />
{/* Don't mind this odd placement, it's just a little debug label */}
{debugTileLayout
? `Tiles generation: ${tileStoreGeneration}`
: undefined}
</div> </div>
)} )}
{showControls && <div className={styles.buttons}>{buttons}</div>} {showControls && <div className={styles.buttons}>{buttons}</div>}

View File

@@ -12,6 +12,7 @@ import { FieldRow, InputField } from "../input/Input";
import { import {
useSetting, useSetting,
duplicateTiles as duplicateTilesSetting, duplicateTiles as duplicateTilesSetting,
debugTileLayout as debugTileLayoutSetting,
} from "./settings"; } from "./settings";
import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { MatrixClient } from "matrix-js-sdk/src/client";
@@ -22,6 +23,9 @@ interface Props {
export const DeveloperSettingsTab: FC<Props> = ({ client }) => { export const DeveloperSettingsTab: FC<Props> = ({ client }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);
const [debugTileLayout, setDebugTileLayout] = useSetting(
debugTileLayoutSetting,
);
return ( return (
<> <>
@@ -70,6 +74,17 @@ export const DeveloperSettingsTab: FC<Props> = ({ client }) => {
)} )}
/> />
</FieldRow> </FieldRow>
<FieldRow>
<InputField
id="debugTileLayout"
type="checkbox"
checked={debugTileLayout}
label={t("developer_mode.debug_tile_layout_label")}
onChange={(event: ChangeEvent<HTMLInputElement>): void =>
setDebugTileLayout(event.target.checked)
}
/>
</FieldRow>
</> </>
); );
}; };

View File

@@ -75,6 +75,8 @@ export const developerSettingsTab = new Setting(
export const duplicateTiles = new Setting("duplicate-tiles", 0); export const duplicateTiles = new Setting("duplicate-tiles", 0);
export const debugTileLayout = new Setting("debug-tile-layout", false);
export const audioInput = new Setting<string | undefined>( export const audioInput = new Setting<string | undefined>(
"audio-input", "audio-input",
undefined, undefined,

View File

@@ -891,10 +891,9 @@ export class CallViewModel extends ViewModel {
this.scope.state(), this.scope.state(),
); );
/** public readonly layoutInternals: Observable<
* The layout of tiles in the call interface. LayoutScanState & { layout: Layout }
*/ > = this.layoutMedia.pipe(
public readonly layout: Observable<Layout> = this.layoutMedia.pipe(
// Each layout will produce a set of tiles, and these tiles have an // Each layout will produce a set of tiles, and these tiles have an
// observable indicating whether they're visible. We loop this information // observable indicating whether they're visible. We loop this information
// back into the layout process by using switchScan. // back into the layout process by using switchScan.
@@ -949,10 +948,26 @@ export class CallViewModel extends ViewModel {
visibleTiles: new Set(), visibleTiles: new Set(),
}, },
), ),
this.scope.state(),
);
/**
* The layout of tiles in the call interface.
*/
public readonly layout: Observable<Layout> = this.layoutInternals.pipe(
map(({ layout }) => layout), map(({ layout }) => layout),
this.scope.state(), this.scope.state(),
); );
/**
* The current generation of the tile store, exposed for debugging purposes.
*/
public readonly tileStoreGeneration: Observable<number> =
this.layoutInternals.pipe(
map(({ tiles }) => tiles.generation),
this.scope.state(),
);
public showSpotlightIndicators: Observable<boolean> = this.layout.pipe( public showSpotlightIndicators: Observable<boolean> = this.layout.pipe(
map((l) => l.type !== "grid"), map((l) => l.type !== "grid"),
this.scope.state(), this.scope.state(),

View File

@@ -6,10 +6,19 @@ Please see LICENSE in the repository root for full details.
*/ */
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { logger } from "matrix-js-sdk/src/logger";
import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel";
import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel";
import { fillGaps } from "../utils/iter"; import { fillGaps } from "../utils/iter";
import { debugTileLayout } from "../settings/settings";
function debugEntries(entries: GridTileData[]): string[] {
return entries.map((e) => e.media.member?.rawDisplayName ?? "[👻]");
}
let DEBUG_ENABLED = false;
debugTileLayout.value.subscribe((value) => (DEBUG_ENABLED = value));
class SpotlightTileData { class SpotlightTileData {
private readonly media_: BehaviorSubject<MediaViewModel[]>; private readonly media_: BehaviorSubject<MediaViewModel[]>;
@@ -69,6 +78,10 @@ export class TileStore {
private constructor( private constructor(
private readonly spotlight: SpotlightTileData | null, private readonly spotlight: SpotlightTileData | null,
private readonly grid: GridTileData[], private readonly grid: GridTileData[],
/**
* A number incremented on each update, just for debugging purposes.
*/
public readonly generation: number,
) {} ) {}
public readonly spotlightTile = this.spotlight?.vm; public readonly spotlightTile = this.spotlight?.vm;
@@ -81,7 +94,7 @@ export class TileStore {
* Creates an an empty collection of tiles. * Creates an an empty collection of tiles.
*/ */
public static empty(): TileStore { public static empty(): TileStore {
return new TileStore(null, []); return new TileStore(null, [], 0);
} }
/** /**
@@ -92,8 +105,9 @@ export class TileStore {
return new TileStoreBuilder( return new TileStoreBuilder(
this.spotlight, this.spotlight,
this.grid, this.grid,
(spotlight, grid) => new TileStore(spotlight, grid), (spotlight, grid) => new TileStore(spotlight, grid, this.generation + 1),
visibleTiles, visibleTiles,
this.generation,
); );
} }
} }
@@ -133,6 +147,10 @@ export class TileStoreBuilder {
grid: GridTileData[], grid: GridTileData[],
) => TileStore, ) => TileStore,
private readonly visibleTiles: Set<GridTileViewModel>, private readonly visibleTiles: Set<GridTileViewModel>,
/**
* A number incremented on each update, just for debugging purposes.
*/
private readonly generation: number,
) {} ) {}
/** /**
@@ -140,6 +158,11 @@ export class TileStoreBuilder {
* will be no spotlight tile. * will be no spotlight tile.
*/ */
public registerSpotlight(media: MediaViewModel[], maximised: boolean): void { public registerSpotlight(media: MediaViewModel[], maximised: boolean): void {
if (DEBUG_ENABLED)
logger.debug(
`[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.member?.rawDisplayName ?? "[👻]")}`,
);
if (this.spotlight !== null) throw new Error("Spotlight already set"); if (this.spotlight !== null) throw new Error("Spotlight already set");
if (this.numGridEntries > 0) if (this.numGridEntries > 0)
throw new Error("Spotlight must be registered before grid tiles"); throw new Error("Spotlight must be registered before grid tiles");
@@ -159,6 +182,11 @@ export class TileStoreBuilder {
* media, then that media will have no grid tile. * media, then that media will have no grid tile.
*/ */
public registerGridTile(media: UserMediaViewModel): void { public registerGridTile(media: UserMediaViewModel): void {
if (DEBUG_ENABLED)
logger.debug(
`[TileStore, ${this.generation}] register grid tile: ${media.member?.rawDisplayName ?? "[👻]"}`,
);
if (this.spotlight !== null) { if (this.spotlight !== null) {
// We actually *don't* want spotlight speakers to appear in both the // We actually *don't* want spotlight speakers to appear in both the
// spotlight and the grid, so they're filtered out here // spotlight and the grid, so they're filtered out here
@@ -246,6 +274,20 @@ export class TileStoreBuilder {
...this.invisibleGridEntries, ...this.invisibleGridEntries,
]), ]),
]; ];
if (DEBUG_ENABLED) {
logger.debug(
`[TileStore, ${this.generation}] stationary: ${debugEntries(this.stationaryGridEntries)}`,
);
logger.debug(
`[TileStore, ${this.generation}] visible: ${debugEntries(this.visibleGridEntries)}`,
);
logger.debug(
`[TileStore, ${this.generation}] invisible: ${debugEntries(this.invisibleGridEntries)}`,
);
logger.debug(
`[TileStore, ${this.generation}] result: ${debugEntries(grid)}`,
);
}
// Destroy unused tiles // Destroy unused tiles
if (this.spotlight === null && this.prevSpotlight !== null) if (this.spotlight === null && this.prevSpotlight !== null)