Kill metrics reducer

Reviewed By: nikoant

Differential Revision: D24332440

fbshipit-source-id: 0a48b25f98d93b181b622e8477a74c7ef0094816
This commit is contained in:
Michel Weststrate
2020-12-15 01:43:06 -08:00
committed by Facebook GitHub Bot
parent 707b8a922e
commit 41a1af33cb
6 changed files with 22 additions and 457 deletions

View File

@@ -33,7 +33,6 @@ export {
} from './plugin';
export {PluginClient, Props} from './plugin';
export {default as Client} from './Client';
export {MetricType} from './utils/exportMetrics';
export {reportUsage} from './utils/metrics';
export {default as promiseTimeout} from './utils/promiseTimeout';
export {clipboard, remote, OpenDialogOptions} from 'electron';

View File

@@ -11,7 +11,6 @@ import {KeyboardActions} from './MenuBar';
import {Logger} from './fb-interfaces/Logger';
import Client from './Client';
import {Store} from './reducers/index';
import {MetricType} from './utils/exportMetrics';
import {ReactNode, Component} from 'react';
import BaseDevice from './devices/BaseDevice';
import {serialize, deserialize} from './utils/serialization';
@@ -131,9 +130,6 @@ export abstract class FlipperBasePlugin<
static defaultPersistedState: any;
static persistedStateReducer: PersistedStateReducer | null;
static maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE;
static metricsReducer:
| ((persistedState: StaticPersistedState) => Promise<MetricType>)
| undefined;
static exportPersistedState:
| ((
callClient: (method: string, params?: any) => Promise<any>,

View File

@@ -1,139 +0,0 @@
/**
* 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 {serialize} from './serialization';
import {State as PluginStatesState} from '../reducers/pluginStates';
import {Store} from '../reducers';
import fs from 'fs';
import {
ExportType,
fetchMetadata,
determinePluginsToProcess,
} from './exportData';
import {deserializeObject} from './serialization';
import {deconstructPluginKey} from './clientUtils';
import {pluginsClassMap} from './pluginUtils';
import {PluginDefinition, isSandyPlugin} from '../plugin';
export type MetricType = {[metricName: string]: number};
type MetricPluginType = {[pluginID: string]: MetricType};
export type ExportMetricType = {[clientID: string]: MetricPluginType};
async function exportMetrics(
pluginStates: PluginStatesState,
pluginsMap: Map<string, PluginDefinition>,
selectedPlugins: Array<string>,
): Promise<string> {
const metrics: ExportMetricType = {};
for (const key in pluginStates) {
const pluginStateData = pluginStates[key];
const plugin = deconstructPluginKey(key);
const pluginName = plugin.pluginName;
if (
pluginName === undefined ||
(selectedPlugins.length > 0 && !selectedPlugins.includes(pluginName))
) {
continue;
}
const client = plugin.client;
const pluginClass = pluginsMap.get(pluginName);
const metricsReducer:
| (<U>(persistedState: U) => Promise<MetricType>)
| undefined =
pluginClass && !isSandyPlugin(pluginClass) // This feature doesn't seem to be used at all, so let's add it when needed for Sandy
? pluginClass.metricsReducer
: undefined;
if (pluginsMap.has(pluginName) && metricsReducer) {
const metricsObject = await metricsReducer(pluginStateData);
const pluginObject: MetricPluginType = {};
pluginObject[pluginName] = metricsObject;
if (!metrics[client]) {
metrics[client] = pluginObject;
continue;
}
const mergedMetrics = {...metrics[client], ...pluginObject};
metrics[client] = mergedMetrics;
}
}
return Promise.resolve(await serialize(metrics));
}
export async function exportMetricsWithoutTrace(
store: Store,
pluginStates: PluginStatesState,
): Promise<string | null> {
const pluginsMap = pluginsClassMap(store.getState().plugins);
const {clients, selectedDevice} = store.getState().connections;
const pluginsToProcess = determinePluginsToProcess(
clients,
selectedDevice,
store.getState().plugins,
);
const metadata = await fetchMetadata(
pluginsToProcess,
pluginStates,
store.getState(),
);
const newPluginStates = metadata.pluginStates;
const {errors} = metadata;
if (errors) {
console.error(errors);
}
const metrics = await exportMetrics(
newPluginStates,
pluginsMap,
store.getState().plugins.selectedPlugins,
);
return metrics;
}
function parseJSON(str: string): any {
try {
return JSON.parse(str);
} catch (e) {
console.error(e);
return undefined;
}
}
export async function exportMetricsFromTrace(
trace: string,
pluginsMap: Map<string, PluginDefinition>,
selectedPlugins: Array<string>,
): Promise<string> {
const data = fs.readFileSync(trace, 'utf8');
const parsedJSONData = parseJSON(data);
if (!parsedJSONData) {
return Promise.reject(
new Error('Please pass the file which has a valid JSON'),
);
}
const importedData: ExportType = deserializeObject(parsedJSONData);
const importedStore = importedData.store;
if (!importedStore) {
return Promise.reject(
new Error(
'No store in the imported file, thus exiting without exporting metrics.',
),
);
}
const {pluginStates} = importedStore;
if (!pluginStates) {
return Promise.reject(
new Error(
'No pluginStates in the imported file, thus exiting without exporting metrics.',
),
);
}
return await exportMetrics(pluginStates, pluginsMap, selectedPlugins);
}

View File

@@ -17,10 +17,6 @@ import dispatcher from '../app/src/dispatcher/index';
import reducers, {Actions, State} from '../app/src/reducers/index';
import {init as initLogger} from '../app/src/fb-stubs/Logger';
import {exportStore} from '../app/src/utils/exportData';
import {
exportMetricsWithoutTrace,
exportMetricsFromTrace,
} from '../app/src/utils/exportMetrics';
import {listDevices} from '../app/src/utils/listDevices';
import setup from '../static/setup';
import {
@@ -42,7 +38,7 @@ type UserArguments = {
dev: boolean;
exit: 'sigint' | 'disconnect';
verbose: boolean;
metrics: string;
metrics: boolean;
listDevices: boolean;
device: string;
listPlugins: boolean;
@@ -116,13 +112,6 @@ type UserArguments = {
.version(global.__VERSION__)
.help().argv; // http://yargs.js.org/docs/#api-argv
function shouldExportMetric(metrics: string): boolean {
if (!metrics) {
return process.argv.includes('--metrics');
}
return true;
}
function outputAndExit(output: string | null | undefined): void {
output = output || '';
console.log(`Finished. Outputting ${output.length} characters.`);
@@ -161,7 +150,7 @@ async function exitActions(
userArguments: UserArguments,
store: Store,
): Promise<void> {
const {metrics, exit} = userArguments;
const {exit} = userArguments;
for (const exitAction of exitClosures) {
try {
const action = await exitAction(userArguments, store);
@@ -176,20 +165,11 @@ async function exitActions(
if (exit == 'sigint') {
process.on('SIGINT', async () => {
try {
if (shouldExportMetric(metrics) && !metrics) {
const state = store.getState();
const payload = await exportMetricsWithoutTrace(
store,
state.pluginStates,
);
outputAndExit(payload);
} else {
const {serializedString, fetchMetaDataErrors} = await exportStore(
store,
);
console.error('Error while fetching metadata', fetchMetaDataErrors);
outputAndExit(serializedString);
}
const {serializedString, fetchMetaDataErrors} = await exportStore(
store,
);
console.error('Error while fetching metadata', fetchMetaDataErrors);
outputAndExit(serializedString);
} catch (e) {
errorAndExit(e);
}
@@ -217,7 +197,7 @@ async function storeModifyingActions(
}
async function startFlipper(userArguments: UserArguments) {
const {verbose, metrics, exit, insecurePort, securePort} = userArguments;
const {verbose, exit, insecurePort, securePort, metrics} = userArguments;
console.error(`
_____ _ _
| __| |_|___ ___ ___ ___
@@ -225,6 +205,11 @@ async function startFlipper(userArguments: UserArguments) {
|__| |_|_| _| _|___|_| v${global.__VERSION__}
|_| |_|
`);
if (metrics) {
throw new Error(
'--metrics is no longer supported, see D24332440 for details.',
);
}
// redirect all logging to stderr
const overriddenMethods = ['debug', 'info', 'log', 'warn', 'error'];
@@ -253,24 +238,13 @@ async function startFlipper(userArguments: UserArguments) {
// TODO(T42325892): Investigate why the export stalls without exiting the
// current eventloop task here.
setTimeout(() => {
if (shouldExportMetric(metrics) && !metrics) {
const state = store.getState();
exportMetricsWithoutTrace(store as Store, state.pluginStates)
.then((payload: string | null) => {
outputAndExit(payload || '');
})
.catch((e: Error) => {
errorAndExit(e);
});
} else {
exportStore(store)
.then(({serializedString}) => {
outputAndExit(serializedString);
})
.catch((e: Error) => {
errorAndExit(e);
});
}
exportStore(store)
.then(({serializedString}) => {
outputAndExit(serializedString);
})
.catch((e: Error) => {
errorAndExit(e);
});
}, 10);
}
return next(action);
@@ -378,19 +352,6 @@ async function startFlipper(userArguments: UserArguments) {
});
},
async (userArguments: UserArguments, store: Store) => {
const {metrics} = userArguments;
if (shouldExportMetric(metrics) && metrics && metrics.length > 0) {
try {
const payload = await exportMetricsFromTrace(
metrics,
pluginsClassMap(store.getState().plugins),
store.getState().plugins.selectedPlugins,
);
return {exit: true, result: payload ? payload.toString() : ''};
} catch (error) {
return {exit: true, result: error};
}
}
return Promise.resolve({exit: false});
},
];

View File

@@ -10,7 +10,7 @@
import FrescoPlugin from '../index';
import {PersistedState, ImageEventWithId} from '../index';
import {AndroidCloseableReferenceLeakEvent} from '../api';
import {MetricType, Notification} from 'flipper';
import {Notification} from 'flipper';
import {ImagesMap} from '../ImagePool';
type ScanDisplayTime = {[scan_number: number]: number};
@@ -63,227 +63,6 @@ function mockPersistedState(
};
}
test('the metric reducer for the input having regression', () => {
const persistedState = mockPersistedState(
[
{
width: 150,
height: 150,
},
{
width: 150,
height: 150,
},
{
width: 150,
height: 150,
},
],
{
width: 100,
height: 100,
},
);
expect(FrescoPlugin.metricsReducer).toBeDefined();
const metrics = FrescoPlugin.metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({
WASTED_BYTES: 37500,
});
});
test('the metric reducer for the input having no regression', () => {
const persistedState = mockPersistedState(
[
{
width: 50,
height: 10,
},
{
width: 50,
height: 50,
},
{
width: 50,
height: 50,
},
],
{
width: 100,
height: 100,
},
);
const metricsReducer = FrescoPlugin.metricsReducer;
expect(metricsReducer).toBeDefined();
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({
WASTED_BYTES: 0,
});
});
test('the metric reducer for the default persisted state', () => {
const metricsReducer = FrescoPlugin.metricsReducer;
expect(metricsReducer).toBeDefined();
const metrics = metricsReducer(FrescoPlugin.defaultPersistedState);
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 0});
});
test('the metric reducer with the events data but with no imageData in imagesMap ', () => {
const persistedState = mockPersistedState(
[
{
width: 50,
height: 10,
},
{
width: 50,
height: 50,
},
{
width: 50,
height: 50,
},
],
{
width: 100,
height: 100,
},
);
persistedState.imagesMap = {};
const metricsReducer = FrescoPlugin.metricsReducer;
expect(metricsReducer).toBeDefined();
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 0});
});
test('the metric reducer with the no viewPort data in events', () => {
const persistedState = mockPersistedState(
[
{
width: 50,
height: 10,
},
{
width: 50,
height: 50,
},
{
width: 50,
height: 50,
},
],
{
width: 100,
height: 100,
},
);
delete persistedState.events[0].viewport;
const metricsReducer = FrescoPlugin.metricsReducer;
expect(metricsReducer).toBeDefined();
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 0});
});
test('the metric reducer with the multiple events', () => {
const scanDisplayTime: ScanDisplayTime = {};
scanDisplayTime[1] = 3;
const events: Array<ImageEventWithId> = [
{
imageIds: ['0', '1'],
eventId: 0,
attribution: [],
startTime: 1,
endTime: 2,
source: 'source',
coldStart: true,
viewport: {width: 100, height: 100, scanDisplayTime},
},
{
imageIds: ['2', '3'],
eventId: 1,
attribution: [],
startTime: 1,
endTime: 2,
source: 'source',
coldStart: true,
viewport: {width: 50, height: 50, scanDisplayTime},
},
];
const imageSizes = [
{
width: 150,
height: 150,
},
{
width: 100,
height: 100,
},
{
width: 250,
height: 250,
},
{
width: 300,
height: 300,
},
];
const imagesMap = imageSizes.reduce((acc, val, index) => {
acc[index] = {
imageId: String(index),
width: val.width,
height: val.height,
sizeBytes: 10,
data: 'undefined',
};
return acc;
}, {} as ImagesMap);
const persistedState = {
surfaceList: new Set<string>(),
images: [],
nextEventId: 0,
events,
imagesMap,
closeableReferenceLeaks: [],
isLeakTrackingEnabled: true,
showDiskImages: false,
};
const metricsReducer = FrescoPlugin.metricsReducer;
expect(metricsReducer).toBeDefined();
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 160000});
});
test('closeable reference metrics on empty state', () => {
const metricsReducer: (
persistedState: PersistedState,
) => Promise<MetricType> = FrescoPlugin.metricsReducer;
const persistedState = mockPersistedState();
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 0});
});
test('closeable reference metrics on input', () => {
const metricsReducer: (
persistedState: PersistedState,
) => Promise<MetricType> = FrescoPlugin.metricsReducer;
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
{
identityHashCode: 'deadbeef',
className: 'com.facebook.imagepipeline.memory.NativeMemoryChunk',
stacktrace: null,
},
{
identityHashCode: 'f4c3b00c',
className: 'com.facebook.flipper.SomeMemoryAbstraction',
stacktrace: null,
},
];
const persistedState = {
...mockPersistedState(),
closeableReferenceLeaks,
};
const metrics = metricsReducer(persistedState);
return expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 2});
});
test('notifications for leaks', () => {
const notificationReducer: (
persistedState: PersistedState,

View File

@@ -19,7 +19,7 @@ import {
} from './api';
import {Fragment} from 'react';
import {ImagesMap} from './ImagePool';
import {MetricType, ReduxState} from 'flipper';
import {ReduxState} from 'flipper';
import React from 'react';
import ImagesCacheOverview from './ImagesCacheOverview';
import {
@@ -207,37 +207,6 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
return persistedState;
};
static metricsReducer = (
persistedState: PersistedState,
): Promise<MetricType> => {
const {events, imagesMap, closeableReferenceLeaks} = persistedState;
const wastedBytes = (events || []).reduce((acc, event) => {
const {viewport, imageIds} = event;
if (!viewport) {
return acc;
}
return imageIds.reduce((innerAcc, imageID) => {
const imageData: ImageData = imagesMap[imageID];
if (!imageData) {
return innerAcc;
}
const imageWidth: number = imageData.width;
const imageHeight: number = imageData.height;
const viewPortWidth: number = viewport.width;
const viewPortHeight: number = viewport.height;
const viewPortArea = viewPortWidth * viewPortHeight;
const imageArea = imageWidth * imageHeight;
return innerAcc + Math.max(0, imageArea - viewPortArea);
}, acc);
}, 0);
return Promise.resolve({
WASTED_BYTES: wastedBytes,
CLOSEABLE_REFERENCE_LEAKS: (closeableReferenceLeaks || []).length,
});
};
static getActiveNotifications = ({
closeableReferenceLeaks = [],
isLeakTrackingEnabled = false,