mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-08 05:34:23 +08:00
[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:
parent
9f78574c3d
commit
a8fbcb0c93
12 changed files with 118 additions and 26 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
},
|
||||
}]
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: =>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -564,6 +564,10 @@ class Actions {
|
|||
static resetEmailCache = ActionScopeGlobal;
|
||||
|
||||
static debugSync = ActionScopeGlobal;
|
||||
|
||||
// Thread list actions
|
||||
static archiveThreads = ActionScopeWindow;
|
||||
static threadListDidUpdate = ActionScopeWindow;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 [];
|
||||
|
|
|
@ -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()
|
|
@ -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');
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue