[client-app] Measure and report archiving times

Summary:
This commit makes so it we report perf metrics for archive actions.
To achieve this, I added a new `ThreadListActionsStore` which serves as
a proxy for thread actions, which allow us to time them.

The new store is in charge of listening to thread list actions, creating and
queueing  the appropriate tasks for any given action, and timing and
reporting action times to our MetricsReporter.

This commit only times archiving actions, and subsequent diffs will time
other relevant thread list actions.

Test Plan: manual

Reviewers: halla, spang, evan

Reviewed By: spang, evan

Differential Revision: https://phab.nylas.com/D3983
This commit is contained in:
Juan Tejada 2017-02-20 16:53:24 -08:00
parent 9f78574c3d
commit a8fbcb0c93
12 changed files with 118 additions and 26 deletions

View file

@ -26,10 +26,10 @@ class ThreadArchiveButton extends React.Component
_onArchive: (e) =>
return unless DOMUtils.nodeIsVisible(e.currentTarget)
tasks = TaskFactory.tasksForArchiving
threads: [@props.thread]
source: "Toolbar Button: Message List"
Actions.queueTasks(tasks)
Actions.archiveThreads({
threads: [@props.thread],
source: 'Toolbar Button: Message List',
})
Actions.popSheet()
e.stopPropagation()

View file

@ -20,11 +20,10 @@ export function sendActions() {
Actions.queueTask(new SendDraftTask(draft.clientId))
return DatabaseStore.modelify(Thread, [draft.threadId])
.then((threads) => {
const tasks = TaskFactory.tasksForArchiving({
Actions.archiveThreads({
source: "Send and Archive",
threads: threads,
})
Actions.queueTasks(tasks)
})
},
}]

View file

@ -103,11 +103,10 @@ export default class ThreadListContextMenu {
return {
label: "Archive",
click: () => {
const tasks = TaskFactory.tasksForArchiving({
Actions.archiveThreads({
source: "Context Menu: Thread List",
threads: this.threads,
})
Actions.queueTasks(tasks)
},
}
}

View file

@ -24,11 +24,11 @@ class ThreadArchiveQuickAction extends React.Component
shouldComponentUpdate: (newProps, newState) ->
newProps.thread.id isnt @props?.thread.id
_onArchive: (event) =>
tasks = TaskFactory.tasksForArchiving
source: "Quick Actions: Thread List"
threads: [@props.thread]
Actions.queueTasks(tasks)
_onArchive: =>
Actions.archiveThreads({
source: "Quick Actions: Thread List",
threads: [@props.thread],
})
# Don't trigger the thread row click
event.stopPropagation()

View file

@ -52,6 +52,11 @@ class ThreadList extends React.Component
ReactDOM.findDOMNode(@).addEventListener('contextmenu', @_onShowContextMenu)
@_onResize()
componentDidUpdate: =>
dataSource = ThreadListStore.dataSource()
threads = dataSource.itemsCurrentlyInView()
Actions.threadListDidUpdate(threads)
componentWillUnmount: =>
@unsub()
window.removeEventListener('resize', @_onResize, true)
@ -318,11 +323,9 @@ class ThreadList extends React.Component
_onArchiveItem: =>
threads = @_threadsForKeyboardAction()
if threads
tasks = TaskFactory.tasksForArchiving
source: "Keyboard Shortcut"
threads: threads
Actions.queueTasks(tasks)
if not threads
return
Actions.archiveThreads({threads, source: "Keyboard Shortcut"})
Actions.popSheet()
_onDeleteItem: =>

View file

@ -22,11 +22,10 @@ export class ArchiveButton extends React.Component {
}
_onArchive = (event) => {
const tasks = TaskFactory.tasksForArchiving({
Actions.archiveThreads({
threads: this.props.items,
source: "Toolbar Button: Thread List",
})
Actions.queueTasks(tasks);
Actions.popSheet();
event.stopPropagation();
return;

View file

@ -11,9 +11,9 @@ export default class GlobalTimer {
this._pendingRuns = {}
}
start(key) {
start(key, now = Date.now()) {
if (!this._pendingRuns[key]) {
this._pendingRuns[key] = [Date.now()]
this._pendingRuns[key] = [now]
}
}
@ -30,15 +30,20 @@ export default class GlobalTimer {
}
}
stop(key) {
stop(key, now = Date.now()) {
if (!this._pendingRuns[key]) { return 0 }
if (!this._doneRuns[key]) { this._doneRuns[key] = [] }
this._pendingRuns[key].push(Date.now());
if (!this._doneRuns[key]) {
this._doneRuns[key] = []
}
this._pendingRuns[key].push(now);
const total = this.calcTotal(this._pendingRuns[key])
this._doneRuns[key].push(this._pendingRuns[key])
if (this._doneRuns[key].length > BUFFER_SIZE) {
this._doneRuns[key].shift()
}
delete this._pendingRuns[key]
return total
}

View file

@ -564,6 +564,10 @@ class Actions {
static resetEmailCache = ActionScopeGlobal;
static debugSync = ActionScopeGlobal;
// Thread list actions
static archiveThreads = ActionScopeWindow;
static threadListDidUpdate = ActionScopeWindow;
}

View file

@ -86,6 +86,11 @@ export default class ObservableListDataSource extends ListTabular.DataSource {
return this._resultSet.offsetOfId(id);
}
itemsCurrentlyInView() {
if (!this._resultSet) { return [] }
return this._resultSet.models()
}
itemsCurrentlyInViewMatching(matchFn) {
if (!this._resultSet) {
return [];

View file

@ -0,0 +1,76 @@
import NylasStore from 'nylas-store'
import {MetricsReporter} from 'isomorphic-core'
import Actions from '../actions'
import Utils from '../models/utils'
import TaskFactory from '../tasks/task-factory'
import IdentityStore from '../stores/identity-store'
class ThreadListActionsStore extends NylasStore {
constructor() {
super()
this._timers = new Map()
}
activate() {
this.listenTo(Actions.archiveThreads, this._onArchiveThreads)
this.listenTo(Actions.threadListDidUpdate, this._onThreadListDidUpdate)
}
deactivate() {
this.stopListeningToAll()
}
_onThreadListDidUpdate = (threads) => {
const updatedAt = Date.now()
const identity = IdentityStore.identity()
if (!identity) { return }
const nylasId = identity.id
const threadIdsInList = new Set(threads.map(t => t.id))
for (const [timerId, timerData] of this._timers.entries()) {
const {threadIds, source, action, accountId, targetCategory} = timerData
const threadsHaveBeenRemoved = threadIds.every(id => !threadIdsInList.has(id))
if (threadsHaveBeenRemoved) {
const actionTimeMs = NylasEnv.timer.stop(timerId, updatedAt)
MetricsReporter.reportEvent({
action,
source,
nylasId,
accountId,
actionTimeMs,
targetCategory,
threadCount: threadIds.length,
})
this._timers.delete(timerId)
}
}
}
_setNewTimer({threads, source, action, targetCategory = 'unknown'} = {}) {
const threadIds = threads.map(t => t.id)
const timerId = Utils.generateTempId()
const timerData = {
source,
action,
threadIds,
targetCategory,
// accountId is irrelevant for metrics reporting but we need to include
// one in order to make a NylasAPIRequest to our /ingest-metrics endpoint
accountId: threads[0].accountId,
}
this._timers.set(timerId, timerData)
NylasEnv.timer.start(timerId)
}
_onArchiveThreads = ({threads, source} = {}) => {
if (threads.length === 0) { return }
this._setNewTimer({threads, source, action: 'remove-from-view', targetCategory: 'archive'})
const tasks = TaskFactory.tasksForArchiving({threads, source})
Actions.queueTasks(tasks)
}
}
export default new ThreadListActionsStore()

View file

@ -164,6 +164,7 @@ lazyLoadAndRegisterStore(`FocusedContentStore`, 'focused-content-store');
lazyLoadAndRegisterStore(`MessageBodyProcessor`, 'message-body-processor');
lazyLoadAndRegisterStore(`FocusedContactsStore`, 'focused-contacts-store');
lazyLoadAndRegisterStore(`TaskQueueStatusStore`, 'task-queue-status-store');
lazyLoadAndRegisterStore(`ThreadListActionsStore`, 'thread-list-actions-store');
lazyLoadAndRegisterStore(`FocusedPerspectiveStore`, 'focused-perspective-store');
lazyLoadAndRegisterStore(`SearchableComponentStore`, 'searchable-component-store');
lazyLoad(`CustomContenteditableComponents`, 'components/overlaid-components/custom-contenteditable-components');

View file

@ -39,7 +39,7 @@ class MetricsReporter {
if (!info.nylasId) {
throw new Error("Metrics Reporter: You must include an nylasId");
}
const logger = global.Logger.child({accountEmail: info.emailAddress})
const logger = global.Logger ? global.Logger.child({accountEmail: info.emailAddress}) : console;
const {workingSetSize, privateBytes, sharedBytes} = process.getProcessMemoryInfo();
info.hostname = os.hostname();
@ -54,6 +54,7 @@ class MetricsReporter {
try {
if (isClientEnv()) {
if (NylasEnv.inDevMode()) { return }
if (!info.accountId) {
throw new Error("Metrics Reporter: You must include an accountId");
}