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:
@@ -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}}",
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user