Skip to content

Commit

Permalink
fix: more improvements to multiquery mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Jul 5, 2022
1 parent 295272d commit c2b80c3
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 51 deletions.
13 changes: 7 additions & 6 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,22 @@ export function fullDesktopQuery(
}

// Performs a query that combines data from multiple devices.
// A multidevice-variant of fullDesktopQuery (with limitations).
//
// Works by constructing a query that fetches events from all devices (using canonicalEvents),
// and then combines the results into a single series of events, which are then processed to
// yield the final output statistics (like fullDesktopQuery).
//
// NOTE: Events from devices are picked in the order of the hostnames array, such that if overlaps are detected the conflict will be resolved by choosing events from the earlier device.
// NOTE: Only supports desktop devices (for now)
// NOTE: Doesn't support browser buckets (and therefore not browser audible detection either)
// This is due to the 'unknown' hostname of browser buckets (will hopefully be fixed soon).
export function multideviceQuery(
hostnames: string[],
filterAFK = true,
classes: [string[], Rule][] = [],
filterCategories: string[][],
include_audible: boolean
filterCategories: string[][]
): string[] {
// NOTE: Only supports desktop devices (and not browser events), for now.
// NOTE: Events from devices are picked in the order of the hostnames array, such that if overlaps are detected the conflict will be resolved by choosing events from the earlier device.

function safeHostname(hostname: string): string {
return hostname.replace(/[^a-zA-Z0-9_]/g, '');
}
Expand All @@ -286,7 +288,6 @@ export function multideviceQuery(
classes: classes,
filter_classes: filterCategories,
filter_afk: filterAFK,
include_audible,
return_variable: 'events_' + safeHostname(hostname),
};

Expand Down
96 changes: 60 additions & 36 deletions src/stores/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ function timeperiodStrsAroundTimeperiod(timeperiod: TimePeriod): string[] {
return timeperiodsAroundTimeperiod(timeperiod).map(timeperiodToStr);
}

function colorCategories(events: IEvent[]): IEvent[] {
// Set $color for categories
const categoryStore = useCategoryStore();
return events.map((e: IEvent) => {
const cat = categoryStore.get_category(e.data['$category']);
e.data['$color'] = getColorFromCategory(cat, categoryStore.classes);
return e;
});
}

interface QueryOptions {
host: string;
date?: string;
Expand Down Expand Up @@ -154,8 +164,20 @@ export const useActivityStore = defineStore('activity', {
// TODO: These queries can actually run in parallel, but since server won't process them in parallel anyway we won't.
await this.set_available(query_options);

// TODO: Move me
const multidevice = true;

if (this.window.available) {
await this.query_desktop_full(query_options);
if (multidevice) {
const hostnames = bucketsStore.hosts.filter(
host =>
bucketsStore.windowBucketsByHost(host).length > 0 && !host.startsWith('fakedata')
);
console.info('Including hosts in multiquery: ', hostnames);
await this.query_multidevice_full(query_options, hostnames);
} else {
await this.query_desktop_full(query_options);
}
} else if (this.android.available) {
await this.query_android(query_options);
} else {
Expand Down Expand Up @@ -246,56 +268,56 @@ export const useActivityStore = defineStore('activity', {
this.query_category_time_by_period_completed(data);
},

async query_multidevice_full(
{ timeperiod, filterCategories, filterAFK }: QueryOptions,
hostnames: string[]
) {
const periods = [timeperiodToStr(timeperiod)];
const classes = loadClassesForQuery();

const q = queries.multideviceQuery(
// TODO: Pass these hostnames in a better way (also consider using device IDs)
hostnames,
filterAFK,
classes,
filterCategories
);
const data = await getClient().query(periods, q);
const data_window = data[0].window;

// Set $color for categories
data_window.cat_events = colorCategories(data_window.cat_events);

this.query_window_completed(data_window);
},

async query_desktop_full({
timeperiod,
filterCategories,
filterAFK,
includeAudible,
}: QueryOptions) {
// TODO: Move this to a separate action?
const multidevice = true;

const periods = [timeperiodToStr(timeperiod)];
const classes = loadClassesForQuery();

let q: string[];
if (!multidevice) {
q = queries.fullDesktopQuery(
this.buckets.browser,
this.buckets.window[0],
this.buckets.afk[0],
filterAFK,
classes,
filterCategories,
includeAudible
);
} else {
q = queries.multideviceQuery(
// TODO: Pass these hostnames in a better way (also consider using device IDs)
['erb-laptop2-arch', 'steamdeck'],
filterAFK,
classes,
filterCategories,
includeAudible
);
}
const q = queries.fullDesktopQuery(
this.buckets.browser,
this.buckets.window[0],
this.buckets.afk[0],
filterAFK,
classes,
filterCategories,
includeAudible
);
const data = await getClient().query(periods, q);
const data_window = data[0].window;

const categoryStore = useCategoryStore();
const data_browser = data[0].browser;

// Set $color for categories
data_window.cat_events = data[0].window['cat_events'].map((e: IEvent) => {
const cat = categoryStore.get_category(e.data['$category']);
e.data['$color'] = getColorFromCategory(cat, categoryStore.classes);
return e;
});
data_window.cat_events = colorCategories(data_window.cat_events);

this.query_window_completed(data_window);
if (!multidevice) {
const data_browser = data[0].browser;
this.query_browser_completed(data_browser);
}
this.query_browser_completed(data_browser);
},

async query_editor({ timeperiod }) {
Expand Down Expand Up @@ -422,6 +444,7 @@ export const useActivityStore = defineStore('activity', {
},

async set_available() {
// TODO: Move to bucketStore on a per-host basis?
this.window.available = this.buckets.afk.length > 0 && this.buckets.window.length > 0;
this.browser.available =
this.buckets.afk.length > 0 &&
Expand All @@ -434,6 +457,7 @@ export const useActivityStore = defineStore('activity', {
},

async get_buckets({ host }) {
// TODO: Move to bucketStore on a per-host basis?
const bucketsStore = useBucketsStore();
this.buckets.afk = bucketsStore.afkBucketsByHost(host);
this.buckets.window = bucketsStore.windowBucketsByHost(host);
Expand Down
54 changes: 46 additions & 8 deletions src/stores/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,44 @@ export const useBucketsStore = defineStore('buckets', {
// TODO: Include consideration of device_id UUID
return _.uniq(_.map(this.buckets, bucket => bucket['device_id']));
},

availableByHost(): (hostname: string) => {
window: boolean;
browser: boolean;
editor: boolean;
} {
// Returns a map of which kinds of buckets are available
//
// 'window' requires ((currentwindow + afkstatus) or android) buckets
// 'browser' requires (currentwindow + afk + browser) buckets
// 'editor' requires editor buckets
return hostname => {
const windowAvailable =
this.windowBucketsByHost(hostname).length > 0 &&
this.afkBucketsByHost(hostname).length > 0;

return {
window: windowAvailable,
browser: windowAvailable && this.browserBuckets(hostname).length > 0,
editor: this.editorBuckets(hostname).length > 0,
};
};
},

availablePerHost(): {
[hostname: string]: { window: boolean; browser: boolean; editor: boolean };
} {
// Returns a map {hostname: {[eg. window, browser, editor]: boolean}} that contains available bucket types for all hosts
// So we want to map over the hosts, and let the values be the result of the availableByHost function for each host.
return Object.assign({}, ...this.hosts().map(this.availableByHost()));
},

// These should be considered low-level, and should be used sparingly.
afkBucketsByHost() {
return host => get_buckets_by_host_and_type(this.buckets, host, 'afkstatus');
return (host: string) => get_buckets_by_host_and_type(this.buckets, host, 'afkstatus');
},
windowBucketsByHost() {
return host =>
return (host: string) =>
_.filter(
get_buckets_by_host_and_type(this.buckets, host, 'currentwindow'),
id => !id.startsWith('aw-watcher-android')
Expand All @@ -50,20 +83,25 @@ export const useBucketsStore = defineStore('buckets', {
);
},
editorBuckets() {
return get_buckets_by_type(this.buckets, 'app.editor.activity');
// fallback to a bucket with 'unknown' host, if one exists.
// TODO: This needs a fix so we can get rid of this workaround.
return (host: string) =>
get_buckets_by_host_and_type(this.buckets, host, 'app.editor.activity') ||
get_buckets_by_host_and_type(this.buckets, 'unknown', 'app.editor.activity');
},
browserBuckets() {
return get_buckets_by_type(this.buckets, 'web.tab.current');
// fallback to a bucket with 'unknown' host, if one exists.
// TODO: This needs a fix so we can get rid of this workaround.
return (host: string) =>
get_buckets_by_host_and_type(this.buckets, host, 'web.tab.current') ||
get_buckets_by_host_and_type(this.buckets, 'unknown', 'web.tab.current');
},
getBucket() {
return id => _.filter(this.buckets, b => b.id === id)[0];
return (id: string) => _.filter(this.buckets, b => b.id === id)[0];
},
bucketsByHostname() {
return _.groupBy(this.buckets, 'hostname');
},
getHostnames() {
return _.map(this.buckets, 'hostname');
},
},

actions: {
Expand Down
2 changes: 1 addition & 1 deletion src/views/Alerts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export default {
mounted: async function () {
await this.bucketsStore.ensureLoaded();
await this.categoryStore.load();
this.hostnames = this.bucketsStore.getHostnames;
this.hostnames = this.bucketsStore.hosts;
this.hostname = this.hostnames[0];
},
methods: {
Expand Down

1 comment on commit c2b80c3

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are screenshots of this commit:

Screenshots using aw-server v0.12.0b2 (click to expand)

CML watermark

Please sign in to comment.