Move app/src (mostly) to flipper-ui-core/src
Summary: This diff moves all UI code from app/src to app/flipper-ui-core. That is now slightly too much (e.g. node deps are not removed yet), but from here it should be easier to move things out again, as I don't want this diff to be open for too long to avoid too much merge conflicts. * But at least flipper-ui-core is Electron free :) * Killed all cross module imports as well, as they where now even more in the way * Some unit test needed some changes, most not too big (but emotion hashes got renumbered in the snapshots, feel free to ignore that) * Found some files that were actually meaningless (tsconfig in plugins, WatchTools files, that start generating compile errors, removed those Follow up work: * make flipper-ui-core configurable, and wire up flipper-server-core in Electron instead of here * remove node deps (aigoncharov) * figure out correct place to load GKs, plugins, make intern requests etc., and move to the correct module * clean up deps Reviewed By: aigoncharov Differential Revision: D32427722 fbshipit-source-id: 14fe92e1ceb15b9dcf7bece367c8ab92df927a70
This commit is contained in:
committed by
Facebook GitHub Bot
parent
54b7ce9308
commit
7e50c0466a
380
desktop/flipper-ui-core/src/dispatcher/tracking.tsx
Normal file
380
desktop/flipper-ui-core/src/dispatcher/tracking.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {performance} from 'perf_hooks';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import {State, Store} from '../reducers/index';
|
||||
import {Logger} from 'flipper-common';
|
||||
import {
|
||||
getPluginBackgroundStats,
|
||||
resetPluginBackgroundStatsDelta,
|
||||
} from '../utils/pluginStats';
|
||||
import {
|
||||
clearTimeline,
|
||||
TrackingEvent,
|
||||
State as UsageTrackingState,
|
||||
selectionChanged,
|
||||
} from '../reducers/usageTracking';
|
||||
import produce from 'immer';
|
||||
import BaseDevice from '../devices/BaseDevice';
|
||||
import {deconstructClientId} from 'flipper-common';
|
||||
import {getCPUUsage} from 'process';
|
||||
import {sideEffect} from '../utils/sideEffect';
|
||||
import {getSelectionInfo} from '../utils/info';
|
||||
import type {SelectionInfo} from '../utils/info';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
const TIME_SPENT_EVENT = 'time-spent';
|
||||
|
||||
type UsageInterval = {
|
||||
selectionKey: string | null;
|
||||
selection: SelectionInfo | null;
|
||||
length: number;
|
||||
focused: boolean;
|
||||
};
|
||||
|
||||
export type UsageSummary = {
|
||||
total: {focusedTime: number; unfocusedTime: number};
|
||||
plugin: {
|
||||
[pluginKey: string]: {
|
||||
focusedTime: number;
|
||||
unfocusedTime: number;
|
||||
} & SelectionInfo;
|
||||
};
|
||||
};
|
||||
|
||||
export const fpsEmitter = new EventEmitter();
|
||||
|
||||
// var is fine, let doesn't have the correct hoisting semantics
|
||||
// eslint-disable-next-line no-var
|
||||
var bytesReceivedEmitter: EventEmitter;
|
||||
|
||||
export function onBytesReceived(
|
||||
callback: (plugin: string, bytes: number) => void,
|
||||
): () => void {
|
||||
if (!bytesReceivedEmitter) {
|
||||
bytesReceivedEmitter = new EventEmitter();
|
||||
}
|
||||
bytesReceivedEmitter.on('bytesReceived', callback);
|
||||
return () => {
|
||||
bytesReceivedEmitter.off('bytesReceived', callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function emitBytesReceived(plugin: string, bytes: number) {
|
||||
if (bytesReceivedEmitter) {
|
||||
bytesReceivedEmitter.emit('bytesReceived', plugin, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
export default (store: Store, logger: Logger) => {
|
||||
const renderHost = getRenderHostInstance();
|
||||
sideEffect(
|
||||
store,
|
||||
{
|
||||
name: 'pluginUsageTracking',
|
||||
throttleMs: 0,
|
||||
noTimeBudgetWarns: true,
|
||||
runSynchronously: true,
|
||||
},
|
||||
getSelectionInfo,
|
||||
(selection, store) => {
|
||||
const time = Date.now();
|
||||
store.dispatch(selectionChanged({selection, time}));
|
||||
},
|
||||
);
|
||||
|
||||
let droppedFrames: number = 0;
|
||||
let largeFrameDrops: number = 0;
|
||||
|
||||
const oldExitData = loadExitData();
|
||||
if (oldExitData) {
|
||||
const isReload = renderHost.processId === oldExitData.pid;
|
||||
const timeSinceLastStartup =
|
||||
Date.now() - parseInt(oldExitData.lastSeen, 10);
|
||||
// console.log(isReload ? 'reload' : 'restart', oldExitData);
|
||||
logger.track('usage', isReload ? 'reload' : 'restart', {
|
||||
...oldExitData,
|
||||
pid: undefined,
|
||||
timeSinceLastStartup,
|
||||
});
|
||||
// create fresh exit data
|
||||
const {selectedDevice, selectedAppId, selectedPlugin} =
|
||||
store.getState().connections;
|
||||
persistExitData(
|
||||
{
|
||||
selectedDevice,
|
||||
selectedAppId,
|
||||
selectedPlugin,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
function droppedFrameDetection(
|
||||
past: DOMHighResTimeStamp,
|
||||
isWindowFocused: () => boolean,
|
||||
) {
|
||||
const now = performance.now();
|
||||
requestAnimationFrame(() => droppedFrameDetection(now, isWindowFocused));
|
||||
const delta = now - past;
|
||||
const dropped = Math.round(delta / (1000 / 60) - 1);
|
||||
fpsEmitter.emit('fps', delta > 1000 ? 0 : Math.round(1000 / (now - past)));
|
||||
if (!isWindowFocused() || dropped < 1) {
|
||||
return;
|
||||
}
|
||||
droppedFrames += dropped;
|
||||
if (dropped > 3) {
|
||||
largeFrameDrops++;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
droppedFrameDetection(
|
||||
performance.now(),
|
||||
() => store.getState().application.windowIsFocused,
|
||||
);
|
||||
}
|
||||
|
||||
renderHost.onIpcEvent('trackUsage', (...args: any[]) => {
|
||||
let state: State;
|
||||
try {
|
||||
state = store.getState();
|
||||
} catch (e) {
|
||||
// if trackUsage is called (indirectly) through a reducer, this will utterly die Flipper. Let's prevent that and log an error instead
|
||||
console.error(
|
||||
'trackUsage triggered indirectly as side effect of a reducer',
|
||||
e,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const {selectedDevice, selectedPlugin, selectedAppId, clients} =
|
||||
state.connections;
|
||||
|
||||
persistExitData(
|
||||
{selectedDevice, selectedPlugin, selectedAppId},
|
||||
args[0] === 'exit',
|
||||
);
|
||||
|
||||
const currentTime = Date.now();
|
||||
const usageSummary = computeUsageSummary(state.usageTracking, currentTime);
|
||||
|
||||
store.dispatch(clearTimeline(currentTime));
|
||||
|
||||
logger.track('usage', TIME_SPENT_EVENT, usageSummary.total);
|
||||
for (const key of Object.keys(usageSummary.plugin)) {
|
||||
logger.track(
|
||||
'usage',
|
||||
TIME_SPENT_EVENT,
|
||||
usageSummary.plugin[key],
|
||||
usageSummary.plugin[key]?.plugin ?? 'none',
|
||||
);
|
||||
}
|
||||
|
||||
Object.entries(state.connections.enabledPlugins).forEach(
|
||||
([app, plugins]) => {
|
||||
// TODO: remove "starred-plugns" event in favor of "enabled-plugins" after some transition period
|
||||
logger.track('usage', 'starred-plugins', {
|
||||
app,
|
||||
starredPlugins: plugins,
|
||||
});
|
||||
logger.track('usage', 'enabled-plugins', {
|
||||
app,
|
||||
enabledPugins: plugins,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const bgStats = getPluginBackgroundStats();
|
||||
logger.track('usage', 'plugin-stats', {
|
||||
cpuTime: bgStats.cpuTime,
|
||||
bytesReceived: bgStats.bytesReceived,
|
||||
});
|
||||
for (const key of Object.keys(bgStats.byPlugin)) {
|
||||
const {
|
||||
cpuTimeTotal: _a,
|
||||
messageCountTotal: _b,
|
||||
bytesReceivedTotal: _c,
|
||||
...dataWithoutTotal
|
||||
} = bgStats.byPlugin[key];
|
||||
if (Object.values(dataWithoutTotal).some((v) => v > 0)) {
|
||||
logger.track('usage', 'plugin-stats-plugin', dataWithoutTotal, key);
|
||||
}
|
||||
}
|
||||
resetPluginBackgroundStatsDelta();
|
||||
|
||||
if (
|
||||
!state.application.windowIsFocused ||
|
||||
!selectedDevice ||
|
||||
!selectedPlugin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let app: string | null = null;
|
||||
let sdkVersion: number | null = null;
|
||||
|
||||
if (selectedAppId) {
|
||||
const client = clients.get(selectedAppId);
|
||||
if (client) {
|
||||
app = client.query.app;
|
||||
sdkVersion = client.query.sdk_version || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
droppedFrames,
|
||||
largeFrameDrops,
|
||||
os: selectedDevice.os,
|
||||
device: selectedDevice.title,
|
||||
plugin: selectedPlugin,
|
||||
app,
|
||||
sdkVersion,
|
||||
isForeground: state.application.windowIsFocused,
|
||||
usedJSHeapSize: (window.performance as any).memory.usedJSHeapSize,
|
||||
cpuLoad: getCPUUsage().percentCPUUsage,
|
||||
};
|
||||
|
||||
// reset dropped frames counter
|
||||
droppedFrames = 0;
|
||||
largeFrameDrops = 0;
|
||||
|
||||
logger.track('usage', 'ping', info);
|
||||
});
|
||||
};
|
||||
|
||||
export function computeUsageSummary(
|
||||
state: UsageTrackingState,
|
||||
currentTime: number,
|
||||
) {
|
||||
const intervals: UsageInterval[] = [];
|
||||
let intervalStart = 0;
|
||||
let isFocused = false;
|
||||
let selection: SelectionInfo | null = null;
|
||||
let selectionKey: string | null;
|
||||
|
||||
function startInterval(event: TrackingEvent) {
|
||||
intervalStart = event.time;
|
||||
if (
|
||||
event.type === 'TIMELINE_START' ||
|
||||
event.type === 'WINDOW_FOCUS_CHANGE'
|
||||
) {
|
||||
isFocused = event.isFocused;
|
||||
}
|
||||
if (event.type === 'SELECTION_CHANGED') {
|
||||
selectionKey = event.selectionKey;
|
||||
selection = event.selection;
|
||||
}
|
||||
}
|
||||
function endInterval(time: number) {
|
||||
const length = time - intervalStart;
|
||||
intervals.push({
|
||||
length,
|
||||
focused: isFocused,
|
||||
selectionKey,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
for (const event of state.timeline) {
|
||||
if (
|
||||
event.type === 'TIMELINE_START' ||
|
||||
event.type === 'WINDOW_FOCUS_CHANGE' ||
|
||||
event.type === 'SELECTION_CHANGED'
|
||||
) {
|
||||
if (event.type !== 'TIMELINE_START') {
|
||||
endInterval(event.time);
|
||||
}
|
||||
startInterval(event);
|
||||
}
|
||||
}
|
||||
endInterval(currentTime);
|
||||
|
||||
return intervals.reduce<UsageSummary>(
|
||||
(acc: UsageSummary, x: UsageInterval) =>
|
||||
produce(acc, (draft) => {
|
||||
draft.total.focusedTime += x.focused ? x.length : 0;
|
||||
draft.total.unfocusedTime += x.focused ? 0 : x.length;
|
||||
const selectionKey = x.selectionKey ?? 'none';
|
||||
draft.plugin[selectionKey] = draft.plugin[selectionKey] ?? {
|
||||
focusedTime: 0,
|
||||
unfocusedTime: 0,
|
||||
...x.selection,
|
||||
};
|
||||
draft.plugin[selectionKey].focusedTime += x.focused ? x.length : 0;
|
||||
draft.plugin[selectionKey].unfocusedTime += x.focused ? 0 : x.length;
|
||||
}),
|
||||
{
|
||||
total: {focusedTime: 0, unfocusedTime: 0},
|
||||
plugin: {},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const flipperExitDataKey = 'FlipperExitData';
|
||||
|
||||
interface ExitData {
|
||||
lastSeen: string;
|
||||
deviceOs: string;
|
||||
deviceType: string;
|
||||
deviceTitle: string;
|
||||
plugin: string;
|
||||
app: string;
|
||||
cleanExit: boolean;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
function loadExitData(): ExitData | undefined {
|
||||
if (!window.localStorage) {
|
||||
return undefined;
|
||||
}
|
||||
const data = window.localStorage.getItem(flipperExitDataKey);
|
||||
if (data) {
|
||||
try {
|
||||
const res = JSON.parse(data);
|
||||
if (res.cleanExit === undefined) {
|
||||
res.cleanExit = true; // avoid skewing results for historical data where this info isn't present
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse flipperExitData', e);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function persistExitData(
|
||||
state: {
|
||||
selectedDevice: BaseDevice | null;
|
||||
selectedPlugin: string | null;
|
||||
selectedAppId: string | null;
|
||||
},
|
||||
cleanExit: boolean,
|
||||
) {
|
||||
if (!window.localStorage) {
|
||||
return;
|
||||
}
|
||||
const exitData: ExitData = {
|
||||
lastSeen: '' + Date.now(),
|
||||
deviceOs: state.selectedDevice ? state.selectedDevice.os : '',
|
||||
deviceType: state.selectedDevice ? state.selectedDevice.deviceType : '',
|
||||
deviceTitle: state.selectedDevice ? state.selectedDevice.title : '',
|
||||
plugin: state.selectedPlugin || '',
|
||||
app: state.selectedAppId
|
||||
? deconstructClientId(state.selectedAppId).app
|
||||
: '',
|
||||
cleanExit,
|
||||
pid: getRenderHostInstance().processId,
|
||||
};
|
||||
window.localStorage.setItem(
|
||||
flipperExitDataKey,
|
||||
JSON.stringify(exitData, null, 2),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user