OLD | NEW |
1 /* | 1 // |
2 * Copyright 2014 Google Inc. All rights reserved. | 2 // Copyright 2014 Google Inc. All rights reserved. |
3 * | 3 // |
4 * Use of this source code is governed by a BSD-style | 4 // Use of this source code is governed by a BSD-style |
5 * license that can be found in the LICENSE file or at | 5 // license that can be found in the LICENSE file or at |
6 * https://developers.google.com/open-source/licenses/bsd | 6 // https://developers.google.com/open-source/licenses/bsd |
7 */ | 7 // |
8 | 8 |
9 part of charted.charts; | 9 part of charted.charts; |
10 | 10 |
11 /** | 11 ///Function callback to filter items in the input |
12 * Function callback to filter items in the input | |
13 */ | |
14 typedef bool AggregationFilterFunc(var item); | 12 typedef bool AggregationFilterFunc(var item); |
15 | 13 |
16 typedef dynamic FieldAccessor(dynamic item, dynamic key); | 14 typedef dynamic FieldAccessor(dynamic item, dynamic key); |
17 | 15 |
18 | 16 /// Given list of items, dimensions and facts, compute |
19 // TODO(midoringo, prsd): Consider splitting each aggregation type into its own | 17 /// aggregates (COUNT, SUM, MIN, MAX) for facts at each dimension level. |
20 // strategy object for readability, maintainability, and scalability. | |
21 /** | |
22 * Given list of items, dimensions and facts, compute | |
23 * aggregates (COUNT, SUM, MIN, MAX) for facts at each dimension level. | |
24 */ | |
25 class AggregationModel { | 18 class AggregationModel { |
26 | |
27 // Number of aggregations we collect on each fact | 19 // Number of aggregations we collect on each fact |
28 int _aggregationTypesCount = 0; | 20 int _aggregationTypesCount = 0; |
29 | 21 |
30 // Currently supported list of aggregations. | 22 // Currently supported list of aggregations. |
31 static final List<String> supportedAggregationTypes = | 23 static final List<String> supportedAggregationTypes = [ |
32 ['sum', 'min', 'max', 'valid']; | 24 'sum', |
| 25 'min', |
| 26 'max', |
| 27 'valid' |
| 28 ]; |
33 | 29 |
34 // Computed aggregation types. | 30 // Computed aggregation types. |
35 List<String> computedAggregationTypes; | 31 List<String> computedAggregationTypes; |
36 | 32 |
37 // Offsets of aggregations that are computed once per fact per dimension | 33 // Offsets of aggregations that are computed once per fact per dimension |
38 // If an offset is null, it will not be computed | 34 // If an offset is null, it will not be computed |
39 int _offsetSum; | 35 int _offsetSum; |
40 int _offsetMin; | 36 int _offsetMin; |
41 int _offsetMax; | 37 int _offsetMax; |
42 int _offsetCnt; | 38 int _offsetCnt; |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
96 final Map<String, Function> comparators; | 92 final Map<String, Function> comparators; |
97 | 93 |
98 // Timing operations | 94 // Timing operations |
99 static final Logger _logger = new Logger('aggregations'); | 95 static final Logger _logger = new Logger('aggregations'); |
100 Stopwatch _timeItWatch; | 96 Stopwatch _timeItWatch; |
101 String _timeItName; | 97 String _timeItName; |
102 | 98 |
103 FieldAccessor dimensionAccessor; | 99 FieldAccessor dimensionAccessor; |
104 FieldAccessor factsAccessor; | 100 FieldAccessor factsAccessor; |
105 | 101 |
106 /** | 102 /// Create a new [AggregationModel] from a [collection] of items, |
107 * Create a new [AggregationModel] from a [collection] of items, | 103 /// list of [dimensions] on which the items are grouped and a list of [facts] |
108 * list of [dimensions] on which the items are grouped and a list of [facts] | 104 /// on which aggregations are computed. |
109 * on which aggregations are computed. | 105 AggregationModel(List collection, List dimensions, List facts, |
110 */ | 106 {List<String> aggregationTypes, |
111 AggregationModel(List collection, List dimensions, | 107 this.walkThroughMap: false, |
112 List facts, | 108 this.comparators, |
113 { List<String> aggregationTypes, | 109 this.dimensionAccessor, |
114 this.walkThroughMap: false, | 110 this.factsAccessor}) { |
115 this.comparators, | |
116 this.dimensionAccessor, | |
117 this.factsAccessor}) { | |
118 _init(collection, dimensions, facts, aggregationTypes); | 111 _init(collection, dimensions, facts, aggregationTypes); |
119 } | 112 } |
120 | 113 |
121 void _timeItStart(String name) { | 114 void _timeItStart(String name) { |
122 _timeItName = name; | 115 _timeItName = name; |
123 _timeItWatch = new Stopwatch(); | 116 _timeItWatch = new Stopwatch(); |
124 _timeItWatch.start(); | 117 _timeItWatch.start(); |
125 } | 118 } |
126 | 119 |
127 void _timeItEnd() { | 120 void _timeItEnd() { |
128 _timeItWatch.stop(); | 121 _timeItWatch.stop(); |
129 _logger.info('[aggregations/$_timeItName] ' | 122 _logger.info('[aggregations/$_timeItName] ' |
130 '${_timeItWatch.elapsed.inMilliseconds}ms/${_rows.length}r'); | 123 '${_timeItWatch.elapsed.inMilliseconds}ms/${_rows.length}r'); |
131 } | 124 } |
132 | 125 |
133 List get factFields => _factFields; | 126 List get factFields => _factFields; |
134 List get dimensionFields => _dimFields; | 127 List get dimensionFields => _dimFields; |
135 | 128 |
136 /** | 129 /// Initialize the view |
137 * Initialize the view | 130 void _init(List collection, List dimensions, List facts, |
138 */ | 131 List<String> aggregationTypes) { |
139 void _init(List collection, List dimensions, | |
140 List facts, List<String> aggregationTypes) { | |
141 if (collection == null) { | 132 if (collection == null) { |
142 throw new ArgumentError('Data cannot be empty or null'); | 133 throw new ArgumentError('Data cannot be empty or null'); |
143 } | 134 } |
144 | 135 |
145 if (facts == null || facts.isEmpty) { | 136 if (facts == null || facts.isEmpty) { |
146 throw new ArgumentError('Facts cannot be empty or null'); | 137 throw new ArgumentError('Facts cannot be empty or null'); |
147 } | 138 } |
148 | 139 |
149 if (dimensions == null) { | 140 if (dimensions == null) { |
150 dimensions = []; | 141 dimensions = []; |
(...skipping 25 matching lines...) Expand all Loading... |
176 | 167 |
177 _rows = collection; | 168 _rows = collection; |
178 _dimFields = new List.from(dimensions, growable: false); | 169 _dimFields = new List.from(dimensions, growable: false); |
179 _factFields = new List.from(facts, growable: false); | 170 _factFields = new List.from(facts, growable: false); |
180 _entityCache = new Map<String, AggregationItem>(); | 171 _entityCache = new Map<String, AggregationItem>(); |
181 | 172 |
182 _createBuffers(); | 173 _createBuffers(); |
183 | 174 |
184 _aggregationTypesCount = aggregationTypes.length; | 175 _aggregationTypesCount = aggregationTypes.length; |
185 for (int i = 0; i < _aggregationTypesCount; i++) { | 176 for (int i = 0; i < _aggregationTypesCount; i++) { |
186 switch(aggregationTypes[i]) { | 177 switch (aggregationTypes[i]) { |
187 case 'sum': | 178 case 'sum': |
188 _offsetSum = i; | 179 _offsetSum = i; |
189 break; | 180 break; |
190 case 'min': | 181 case 'min': |
191 _offsetMin = i; | 182 _offsetMin = i; |
192 break; | 183 break; |
193 case 'max': | 184 case 'max': |
194 _offsetMax = i; | 185 _offsetMax = i; |
195 break; | 186 break; |
196 case 'valid': | 187 case 'valid': |
197 _offsetCnt = i; | 188 _offsetCnt = i; |
198 break; | 189 break; |
199 } | 190 } |
200 } | 191 } |
201 computedAggregationTypes = new List.from(aggregationTypes, growable: false); | 192 computedAggregationTypes = new List.from(aggregationTypes, growable: false); |
202 | 193 |
203 // Preprocess the data | 194 // Preprocess the data |
204 _preprocess(); | 195 _preprocess(); |
205 } | 196 } |
206 | 197 |
207 /** | 198 /// Re-calculate aggregations based on new dimensions. |
208 * Re-calculate aggregations based on new dimensions. | |
209 */ | |
210 void groupBy(List dimensions, [AggregationFilterFunc filter = null]) { | 199 void groupBy(List dimensions, [AggregationFilterFunc filter = null]) { |
211 if (dimensions == null) { | 200 if (dimensions == null) { |
212 dimensions = []; | 201 dimensions = []; |
213 } | 202 } |
214 | 203 |
215 List savedDimFields = _dimFields; | 204 List savedDimFields = _dimFields; |
216 _dimFields = new List.from(dimensions, growable: false); | 205 _dimFields = new List.from(dimensions, growable: false); |
217 | 206 |
218 _dimPrefixLength = 0; | 207 _dimPrefixLength = 0; |
219 while (_dimPrefixLength < _dimFields.length && | 208 while (_dimPrefixLength < _dimFields.length && |
220 _dimPrefixLength < savedDimFields.length && | 209 _dimPrefixLength < savedDimFields.length && |
221 savedDimFields[_dimPrefixLength] == _dimFields[_dimPrefixLength]) { | 210 savedDimFields[_dimPrefixLength] == _dimFields[_dimPrefixLength]) { |
222 ++_dimPrefixLength; | 211 ++_dimPrefixLength; |
223 } | 212 } |
224 | 213 |
225 _createBuffers(); | 214 _createBuffers(); |
226 _preprocess(groupBy:true); | 215 _preprocess(groupBy: true); |
227 | 216 |
228 // For groupBy, compute immediately. | 217 // For groupBy, compute immediately. |
229 compute(filter); | 218 compute(filter); |
230 | 219 |
231 // Ensure that cache represents updated dimensions | 220 // Ensure that cache represents updated dimensions |
232 _updateCachedEntities(); | 221 _updateCachedEntities(); |
233 } | 222 } |
234 | 223 |
235 /** | 224 /// Create buffers. |
236 * Create buffers. | 225 /// |
237 * This method is called when the object is being created and when | 226 /// This method is called when the object is being created and when |
238 * a groupBy is called to change the dimensions on which | 227 /// a groupBy is called to change the dimensions on which |
239 * aggregations are computed. | 228 /// aggregations are computed. |
240 */ | |
241 void _createBuffers() { | 229 void _createBuffers() { |
242 // Create both when object is created and groupBy is called | 230 // Create both when object is created and groupBy is called |
243 _dimEnumCache = new Int32List(_dimFields.length * _rows.length); | 231 _dimEnumCache = new Int32List(_dimFields.length * _rows.length); |
244 | 232 |
245 // Create only when the object is created | 233 // Create only when the object is created |
246 if (_factsCache == null) { | 234 if (_factsCache == null) { |
247 _factsCache = new Float64List((_factFields.length + 1) * _rows.length); | 235 _factsCache = new Float64List((_factFields.length + 1) * _rows.length); |
248 } | 236 } |
249 | 237 |
250 // Create only when the object is created | 238 // Create only when the object is created |
251 if (_filterResults == null) { | 239 if (_filterResults == null) { |
252 _filterResults = new List<int>((_rows.length) ~/ SMI_BITS + 1); | 240 _filterResults = new List<int>((_rows.length) ~/ SMI_BITS + 1); |
253 } | 241 } |
254 | 242 |
255 // Create both when object is created and groupBy is called | 243 // Create both when object is created and groupBy is called |
256 // Reuse dimension enumerations if possible. | 244 // Reuse dimension enumerations if possible. |
257 var oldDimToInt = _dimToIntMap; | 245 var oldDimToInt = _dimToIntMap; |
258 _dimToIntMap = new List<Map<dynamic, int>>.generate(_dimFields.length, | 246 _dimToIntMap = new List<Map<dynamic, int>>.generate(_dimFields.length, |
259 (i) => i < _dimPrefixLength ? oldDimToInt[i] : new Map<dynamic, int>()); | 247 (i) => i < _dimPrefixLength ? oldDimToInt[i] : new Map<dynamic, int>()); |
260 } | 248 } |
261 | 249 |
262 /** | 250 /// Check cache entries |
263 * Check cache entries | 251 /// When data is grouped by a new dimensions, entities that were |
264 * When data is grouped by a new dimensions, entities that were | 252 /// created prior to the groupBy should be cleared and removed from cache |
265 * created prior to the groupBy should be cleared and removed from cache | 253 /// if they aren't valid anymore. |
266 * if they aren't valid anymore. | 254 /// Update the entities that are valid after the groupBy. |
267 * Update the entities that are valid after the groupBy. | |
268 */ | |
269 void _updateCachedEntities() { | 255 void _updateCachedEntities() { |
270 List keys = new List.from(_entityCache.keys, growable: false); | 256 List keys = new List.from(_entityCache.keys, growable: false); |
271 keys.forEach((key) { | 257 keys.forEach((key) { |
272 _AggregationItemImpl entity = _entityCache[key]; | 258 _AggregationItemImpl entity = _entityCache[key]; |
273 if (entity == null) { | 259 if (entity == null) { |
274 _entityCache.remove(key); | 260 _entityCache.remove(key); |
275 } else if (entity != null && entity.isValid) { | 261 } else if (entity != null && entity.isValid) { |
276 if (key.split(':').length <= _dimPrefixLength) { | 262 if (key.split(':').length <= _dimPrefixLength) { |
277 entity.update(); | 263 entity.update(); |
278 } else { | 264 } else { |
279 _entityCache.remove(key); | 265 _entityCache.remove(key); |
280 entity.clear(); | 266 entity.clear(); |
281 } | 267 } |
282 } | 268 } |
283 }); | 269 }); |
284 } | 270 } |
285 | 271 |
286 final Map<String, List> _parsedKeys = {}; | 272 final Map<String, List> _parsedKeys = {}; |
287 /** | 273 /// Get value from a map-like object |
288 * Get value from a map-like object | |
289 */ | |
290 dynamic _fetch(var item, String key) { | 274 dynamic _fetch(var item, String key) { |
291 if (walkThroughMap && key.contains('.')) { | 275 if (walkThroughMap && key.contains('.')) { |
292 return walk(item, key, _parsedKeys); | 276 return walk(item, key, _parsedKeys); |
293 } else { | 277 } else { |
294 return item[key]; | 278 return item[key]; |
295 } | 279 } |
296 } | 280 } |
297 | 281 |
298 /* | 282 /// Preprocess Data |
299 * Preprocess Data | 283 /// - Enumerate dimension values |
300 * - Enumerate dimension values | 284 /// - Create sort orders for dimension values |
301 * - Create sort orders for dimension values | 285 /// - Cache facts in lists |
302 * - Cache facts in lists | |
303 */ | |
304 void _preprocess({bool groupBy: false}) { | 286 void _preprocess({bool groupBy: false}) { |
305 | |
306 _timeItStart('preprocess'); | 287 _timeItStart('preprocess'); |
307 | 288 |
308 // Enumerate dimensions... | 289 // Enumerate dimensions... |
309 // Cache dimensions and facts. | 290 // Cache dimensions and facts. |
310 | 291 |
311 List<int> dimensionValCount = | 292 List<int> dimensionValCount = |
312 new List<int>.generate(_dimFields.length, (idx) => 0); | 293 new List<int>.generate(_dimFields.length, (idx) => 0); |
313 | 294 |
314 int dimensionsCount = _dimFields.length; | 295 int dimensionsCount = _dimFields.length; |
315 int factsCount = _factFields.length; | 296 int factsCount = _factFields.length; |
316 int rowCount = _rows.length; | 297 int rowCount = _rows.length; |
317 | 298 |
318 for (int ri = 0, factsDataOffset = 0, dimDataOffset = 0; | 299 for (int ri = 0, factsDataOffset = 0, dimDataOffset = 0; |
319 ri < rowCount; ++ri, factsDataOffset += factsCount, | 300 ri < rowCount; |
320 dimDataOffset += dimensionsCount) { | 301 ++ri, factsDataOffset += factsCount, dimDataOffset += dimensionsCount) { |
321 var item = _rows[ri]; | 302 var item = _rows[ri]; |
322 | 303 |
323 // Cache the fact values in the big buffer, but only | 304 // Cache the fact values in the big buffer, but only |
324 // when we are initializing (not when a groupBy was called | 305 // when we are initializing (not when a groupBy was called |
325 // after initialization) | 306 // after initialization) |
326 if (!groupBy) { | 307 if (!groupBy) { |
327 for (int fi = 0; fi < factsCount; fi++) { | 308 for (int fi = 0; fi < factsCount; fi++) { |
328 var value = factsAccessor(item,_factFields[fi]); | 309 var value = factsAccessor(item, _factFields[fi]); |
329 _factsCache[factsDataOffset + fi] = | 310 _factsCache[factsDataOffset + fi] = |
330 (value == null) ? double.NAN : value.toDouble(); | 311 (value == null) ? double.NAN : value.toDouble(); |
331 } | 312 } |
332 } | 313 } |
333 | 314 |
334 // Enumerate the dimension values and cache enumerated rows | 315 // Enumerate the dimension values and cache enumerated rows |
335 for (int di = 0; di < dimensionsCount; di++) { | 316 for (int di = 0; di < dimensionsCount; di++) { |
336 var dimensionVal = dimensionAccessor(item, _dimFields[di]); | 317 var dimensionVal = dimensionAccessor(item, _dimFields[di]); |
337 int dimensionValEnum = _dimToIntMap[di][dimensionVal]; | 318 int dimensionValEnum = _dimToIntMap[di][dimensionVal]; |
338 if (dimensionValEnum == null) { | 319 if (dimensionValEnum == null) { |
(...skipping 12 matching lines...) Expand all Loading... |
351 if (groupBy && i < _dimPrefixLength) { | 332 if (groupBy && i < _dimPrefixLength) { |
352 return oldSortOrders[i]; | 333 return oldSortOrders[i]; |
353 } | 334 } |
354 | 335 |
355 List dimensionVals = new List.from(_dimToIntMap[i].keys); | 336 List dimensionVals = new List.from(_dimToIntMap[i].keys); |
356 List retval = new List(_dimToIntMap[i].length); | 337 List retval = new List(_dimToIntMap[i].length); |
357 | 338 |
358 // When a comparator is not specified, our implementation of the | 339 // When a comparator is not specified, our implementation of the |
359 // comparator tries to gracefully handle null values. | 340 // comparator tries to gracefully handle null values. |
360 dimensionVals.sort( | 341 dimensionVals.sort( |
361 comparators != null && comparators.containsKey(_dimFields[i]) ? | 342 comparators != null && comparators.containsKey(_dimFields[i]) |
362 comparators[_dimFields[i]] : _defaultDimComparator); | 343 ? comparators[_dimFields[i]] |
| 344 : _defaultDimComparator); |
363 | 345 |
364 for (int si = 0; si < retval.length; ++si) { | 346 for (int si = 0; si < retval.length; ++si) { |
365 retval[_dimToIntMap[i][dimensionVals[si]]] = si; | 347 retval[_dimToIntMap[i][dimensionVals[si]]] = si; |
366 } | 348 } |
367 return retval; | 349 return retval; |
368 }, growable: false); | 350 }, growable: false); |
369 | 351 |
370 // Create a list of sorted indices - only if we are not having a full | 352 // Create a list of sorted indices - only if we are not having a full |
371 // overlap of dimensionFields. | 353 // overlap of dimensionFields. |
372 if (!groupBy || _dimPrefixLength != _dimFields.length) { | 354 if (!groupBy || _dimPrefixLength != _dimFields.length) { |
373 _sorted = new List<int>.generate(_rows.length, (i) => i, growable: false); | 355 _sorted = new List<int>.generate(_rows.length, (i) => i, growable: false); |
374 _sorted.sort(_comparator); | 356 _sorted.sort(_comparator); |
375 } | 357 } |
376 | 358 |
377 // Pre-compute frequently used values | 359 // Pre-compute frequently used values |
378 _offsetSortedIndex = factsCount * _aggregationTypesCount; | 360 _offsetSortedIndex = factsCount * _aggregationTypesCount; |
379 _offsetFilteredCount = factsCount * _aggregationTypesCount + 1; | 361 _offsetFilteredCount = factsCount * _aggregationTypesCount + 1; |
380 | 362 |
381 _timeItEnd(); | 363 _timeItEnd(); |
382 } | 364 } |
383 | 365 |
384 // Ensures that null dimension values don't cause an issue with sorting | 366 // Ensures that null dimension values don't cause an issue with sorting |
385 _defaultDimComparator(Comparable left, Comparable right) => | 367 _defaultDimComparator(Comparable left, Comparable right) => |
386 (left == null && right == null) ? 0 : | 368 (left == null && right == null) |
387 (left == null) ? -1 : | 369 ? 0 |
388 (right == null) ? 1 : left.compareTo(right); | 370 : (left == null) ? -1 : (right == null) ? 1 : left.compareTo(right); |
389 | 371 |
390 /* | 372 /// Given item indices in rows, compare them based |
391 * Given item indices in rows, compare them based | 373 /// on the sort orders created while pre-processing data. |
392 * on the sort orders created while preprocessing data. | |
393 */ | |
394 _comparator(int one, int two) { | 374 _comparator(int one, int two) { |
395 if (one == two) { | 375 if (one == two) { |
396 return 0; | 376 return 0; |
397 } | 377 } |
398 | 378 |
399 int offsetOne = _dimFields.length * one; | 379 int offsetOne = _dimFields.length * one; |
400 int offsetTwo = _dimFields.length * two; | 380 int offsetTwo = _dimFields.length * two; |
401 | 381 |
402 for (int i = 0; i < _dimFields.length; ++i) { | 382 for (int i = 0; i < _dimFields.length; ++i) { |
403 int diff = _dimSortOrders[i][_dimEnumCache[offsetOne + i]] - | 383 int diff = _dimSortOrders[i][_dimEnumCache[offsetOne + i]] - |
404 _dimSortOrders[i][_dimEnumCache[offsetTwo + i]]; | 384 _dimSortOrders[i][_dimEnumCache[offsetTwo + i]]; |
405 if (diff != 0) { | 385 if (diff != 0) { |
406 return diff; | 386 return diff; |
407 } | 387 } |
408 } | 388 } |
409 return 0; | 389 return 0; |
410 } | 390 } |
411 | 391 |
412 /** | 392 /// Compute aggregations |
413 * Compute aggregations | 393 /// If [filter] is not null, it would be used to filter out items that |
414 * If [filter] is not null, it would be used to filter out items that | 394 /// should not be included in the aggregates. |
415 * should not be included in the aggregates. | |
416 */ | |
417 void compute([AggregationFilterFunc filter = null]) { | 395 void compute([AggregationFilterFunc filter = null]) { |
418 _timeItStart('compute'); | 396 _timeItStart('compute'); |
419 | 397 |
420 _dimToAggrMap = new Map<String, int>(); | 398 _dimToAggrMap = new Map<String, int>(); |
421 _aggregations = new Float64List(AGGREGATIONS_BUFFER_LENGTH); | 399 _aggregations = new Float64List(AGGREGATIONS_BUFFER_LENGTH); |
422 _filterResults = filter == null ? | 400 _filterResults = filter == null |
423 null : new List<int>.filled((_rows.length ~/ SMI_BITS) + 1, 0); | 401 ? null |
| 402 : new List<int>.filled((_rows.length ~/ SMI_BITS) + 1, 0); |
424 | 403 |
425 int rowCount = _rows.length; | 404 int rowCount = _rows.length; |
426 int dimensionCount = _dimFields.length; | 405 int dimensionCount = _dimFields.length; |
427 int factsCount = _factFields.length; | 406 int factsCount = _factFields.length; |
428 | 407 |
429 // Saves current dimension value to which we are aggregating | 408 // Saves current dimension value to which we are aggregating |
430 // Values of dimensions are in even indices (starting at 0) and | 409 // Values of dimensions are in even indices (starting at 0) and |
431 // location of respective dimension in buffer is in odd indices. | 410 // location of respective dimension in buffer is in odd indices. |
432 List<int> currentDim = new List<int>(dimensionCount * 2); | 411 List<int> currentDim = new List<int>(dimensionCount * 2); |
433 bool reset = true; | 412 bool reset = true; |
434 bool isNewDimension = false; | 413 bool isNewDimension = false; |
435 int aggregationSizePerDim = factsCount * _aggregationTypesCount; | 414 int aggregationSizePerDim = factsCount * _aggregationTypesCount; |
436 | 415 |
437 // Reserve the 0th position for top-level aggregations. | 416 // Reserve the 0th position for top-level aggregations. |
438 int currentBufferPos = (factsCount * _aggregationTypesCount + 2); | 417 int currentBufferPos = (factsCount * _aggregationTypesCount + 2); |
439 _dimToAggrMap[''] = 0; | 418 _dimToAggrMap[''] = 0; |
440 _aggregations[_offsetSortedIndex] = 0.0; | 419 _aggregations[_offsetSortedIndex] = 0.0; |
441 | 420 |
442 | |
443 for (int ri = 0, index = 0, dimensionDataOffset = 0, factsDataOffset = 0; | 421 for (int ri = 0, index = 0, dimensionDataOffset = 0, factsDataOffset = 0; |
444 ri < rowCount; ++ri, reset = false) { | 422 ri < rowCount; |
445 | 423 ++ri, reset = false) { |
446 // If filter is not null, check if this row must be included in | 424 // If filter is not null, check if this row must be included in |
447 // the aggregations and mark it accordingly. | 425 // the aggregations and mark it accordingly. |
448 index = _sorted[ri]; | 426 index = _sorted[ri]; |
449 if (filter != null) { | 427 if (filter != null) { |
450 if (!filter(_rows[index])) { | 428 if (!filter(_rows[index])) { |
451 continue; | 429 continue; |
452 } else { | 430 } else { |
453 _filterResults[ri ~/ SMI_BITS] |= (1 << (ri % SMI_BITS)); | 431 _filterResults[ri ~/ SMI_BITS] |= (1 << (ri % SMI_BITS)); |
454 } | 432 } |
455 } | 433 } |
456 | 434 |
457 dimensionDataOffset = index * dimensionCount; | 435 dimensionDataOffset = index * dimensionCount; |
458 factsDataOffset = index * factsCount; | 436 factsDataOffset = index * factsCount; |
459 | 437 |
460 // Update top-level aggregations. | 438 // Update top-level aggregations. |
461 _updateAggregationsAt(0, factsDataOffset, ri == 0 ? true : false); | 439 _updateAggregationsAt(0, factsDataOffset, ri == 0 ? true : false); |
462 | 440 |
463 // See which dimensions get effected by this row. | 441 // See which dimensions get effected by this row. |
464 // di => the index of the dimension | 442 // di => the index of the dimension |
465 // ci => index of the cached value in [currentDim] | 443 // ci => index of the cached value in [currentDim] |
466 for (int di = 0, ci = 0; di < dimensionCount; ++di, ci += 2) { | 444 for (int di = 0, ci = 0; di < dimensionCount; ++di, ci += 2) { |
467 // If a dimension value changed, then all dimensions that are lower | 445 // If a dimension value changed, then all dimensions that are lower |
468 // in the hierarchy change too. | 446 // in the hierarchy change too. |
469 if (reset || | 447 if (reset || |
470 currentDim[ci] != _dimEnumCache[dimensionDataOffset + di]) { | 448 currentDim[ci] != _dimEnumCache[dimensionDataOffset + di]) { |
471 currentDim[ci] = _dimEnumCache[dimensionDataOffset + di]; | 449 currentDim[ci] = _dimEnumCache[dimensionDataOffset + di]; |
472 currentDim[ci + 1] = currentBufferPos; | 450 currentDim[ci + 1] = currentBufferPos; |
473 | 451 |
474 // Save location to aggregations position in the buffer | 452 // Save location to aggregations position in the buffer |
475 _dimToAggrMap[new List.generate(di + 1, | 453 _dimToAggrMap[new List.generate(di + 1, (i) => currentDim[2 * i]) |
476 (i) => currentDim[2 * i]).join(':')] = currentBufferPos; | 454 .join(':')] = currentBufferPos; |
477 | 455 |
478 // Store items start position | 456 // Store items start position |
479 _aggregations[currentBufferPos + _offsetSortedIndex] = ri.toDouble(); | 457 _aggregations[currentBufferPos + _offsetSortedIndex] = ri.toDouble(); |
480 | 458 |
481 // After the aggregated values, we save the filtered count, | 459 // After the aggregated values, we save the filtered count, |
482 // index of the first item (in sorted) | 460 // index of the first item (in sorted) |
483 currentBufferPos += (aggregationSizePerDim + 2); | 461 currentBufferPos += (aggregationSizePerDim + 2); |
484 reset = true; | 462 reset = true; |
485 isNewDimension = true; | 463 isNewDimension = true; |
486 } | 464 } |
487 | 465 |
488 _updateAggregationsAt(currentDim[ci + 1], | 466 _updateAggregationsAt( |
489 factsDataOffset, isNewDimension); | 467 currentDim[ci + 1], factsDataOffset, isNewDimension); |
490 isNewDimension = false; | 468 isNewDimension = false; |
491 } | 469 } |
492 } | 470 } |
493 | 471 |
494 _timeItEnd(); | 472 _timeItEnd(); |
495 } | 473 } |
496 | 474 |
497 /** | 475 /// Helper function that does the actual aggregations. |
498 * Helper function that does the actual aggregations. | 476 /// This function is called once per row per dimension. |
499 * This function is called once per row per dimension. | 477 _updateAggregationsAt( |
500 */ | 478 int aggrDataOffset, int factsDataOffset, bool isNewDimension) { |
501 _updateAggregationsAt(int aggrDataOffset, | |
502 int factsDataOffset, bool isNewDimension) { | |
503 // Update count. | 479 // Update count. |
504 _aggregations[aggrDataOffset + _offsetFilteredCount] += 1; | 480 _aggregations[aggrDataOffset + _offsetFilteredCount] += 1; |
505 | 481 |
506 // Update aggregation for each of the facts. | 482 // Update aggregation for each of the facts. |
507 for (int fi = 0, bufferFactOffset = aggrDataOffset; | 483 for (int fi = 0, bufferFactOffset = aggrDataOffset; |
508 fi < _factFields.length; | 484 fi < _factFields.length; |
509 bufferFactOffset += _aggregationTypesCount, ++fi) { | 485 bufferFactOffset += _aggregationTypesCount, ++fi) { |
510 | |
511 double factValue = _factsCache[factsDataOffset + fi]; | 486 double factValue = _factsCache[factsDataOffset + fi]; |
512 if (factValue.isNaN) { | 487 if (factValue.isNaN) { |
513 continue; | 488 continue; |
514 } | 489 } |
515 | 490 |
516 // Sum | 491 // Sum |
517 if (_offsetSum != null) { | 492 if (_offsetSum != null) { |
518 _aggregations[bufferFactOffset + _offsetSum] += factValue; | 493 _aggregations[bufferFactOffset + _offsetSum] += factValue; |
519 } | 494 } |
520 | 495 |
521 // Min | 496 // Min |
522 if (_offsetMin != null && (isNewDimension || factValue < | 497 if (_offsetMin != null && |
523 _aggregations[bufferFactOffset + _offsetMin])) { | 498 (isNewDimension || |
| 499 factValue < _aggregations[bufferFactOffset + _offsetMin])) { |
524 _aggregations[bufferFactOffset + _offsetMin] = factValue; | 500 _aggregations[bufferFactOffset + _offsetMin] = factValue; |
525 } | 501 } |
526 | 502 |
527 // Max | 503 // Max |
528 if (_offsetMax != null && (isNewDimension || factValue > | 504 if (_offsetMax != null && |
529 _aggregations[bufferFactOffset + _offsetMax])) { | 505 (isNewDimension || |
| 506 factValue > _aggregations[bufferFactOffset + _offsetMax])) { |
530 _aggregations[bufferFactOffset + _offsetMax] = factValue; | 507 _aggregations[bufferFactOffset + _offsetMax] = factValue; |
531 } | 508 } |
532 | 509 |
533 // Count | 510 // Count |
534 if (_offsetCnt != null) { | 511 if (_offsetCnt != null) { |
535 _aggregations[bufferFactOffset + _offsetCnt]++; | 512 _aggregations[bufferFactOffset + _offsetCnt]++; |
536 } | 513 } |
537 } | 514 } |
538 } | 515 } |
539 | 516 |
540 /* | 517 // TODO(prsd): |
541 * TODO(prsd): | 518 // 1. Implementation of updates and posting updates to entities. |
542 * 1. Implementation of updates and posting updates to entities. | 519 // patchEntity and addToEntity must add listeners on AggregationItems |
543 * patchEntity and addToEntity must add listeners on AggregationItems | 520 // and any changes must be propagated to entities. |
544 * and any changes must be propagated to entities. | 521 // 2. Updates (add/remove/update) should do their best to update the |
545 * 2. Updates (add/remove/update) should do their best to update the | 522 // aggregations and then maybe do a delayed recomputation (sort etc;) |
546 * aggregations and then maybe do a delayed recomputation (sort etc;) | |
547 */ | |
548 | 523 |
549 /** | 524 /// Update an item. |
550 * Update an item. | 525 /// If aggregates were already computed, they are updated to reflect the |
551 * If aggregates were already computed, they are updated to reflect the | 526 /// new value and any observers are notified. |
552 * new value and any observers are notified. | |
553 */ | |
554 void updateItem(dynamic item, String field) { | 527 void updateItem(dynamic item, String field) { |
555 throw new UnimplementedError(); | 528 throw new UnimplementedError(); |
556 } | 529 } |
557 | 530 |
558 /** | 531 /// Add a new item. |
559 * Add a new item. | 532 /// If aggregates were already computed, they are updated to reflect |
560 * If aggregates were already computed, they are updated to reflect | 533 /// values on the new item. |
561 * values on the new item. | |
562 */ | |
563 void addItem(dynamic item) { | 534 void addItem(dynamic item) { |
564 throw new UnimplementedError(); | 535 throw new UnimplementedError(); |
565 } | 536 } |
566 | 537 |
567 /** | 538 /// Remove an existing item. |
568 * Remove an existing item. | 539 /// If aggregates were already computed, they are updated to reflect |
569 * If aggregates were already computed, they are updated to reflect | 540 /// facts on the removed item. |
570 * facts on the removed item. | |
571 */ | |
572 void removeItem(dynamic item) { | 541 void removeItem(dynamic item) { |
573 throw new UnimplementedError(); | 542 throw new UnimplementedError(); |
574 } | 543 } |
575 | 544 |
576 /** | 545 /// Return an [AggregationItem] that represents facts for dimension |
577 * Return an [AggregationItem] that represents facts for dimension | 546 /// represented by [dimension] Only one instance of an entity is created |
578 * represented by [dimension] Only one instance of an entity is created | 547 /// per dimension (even if this function is called multiple times) |
579 * per dimension (even if this function is called multiple times) | 548 /// |
580 * | 549 /// Callers of this method can observe the returned entity for updates to |
581 * Callers of this method can observe the returned entity for updates to | 550 /// aggregations caused by changes to filter or done through add, remove |
582 * aggregations caused by changes to filter or done through add, remove | 551 /// or modify of items in the collection. |
583 * or modify of items in the collection. | |
584 */ | |
585 AggregationItem facts(List dimension) { | 552 AggregationItem facts(List dimension) { |
586 List<int> enumeratedList = new List<int>(); | 553 List<int> enumeratedList = new List<int>(); |
587 for (int i = 0; i < dimension.length; ++i) { | 554 for (int i = 0; i < dimension.length; ++i) { |
588 enumeratedList.add(_dimToIntMap[i][dimension[i]]); | 555 enumeratedList.add(_dimToIntMap[i][dimension[i]]); |
589 } | 556 } |
590 | 557 |
591 String key = enumeratedList.join(':'); | 558 String key = enumeratedList.join(':'); |
592 AggregationItem item = _entityCache[key]; | 559 AggregationItem item = _entityCache[key]; |
593 | 560 |
594 if (item == null && _dimToAggrMap.containsKey(key)) { | 561 if (item == null && _dimToAggrMap.containsKey(key)) { |
595 item = new _AggregationItemImpl(this, dimension, key); | 562 item = new _AggregationItemImpl(this, dimension, key); |
596 _entityCache[key] = item; | 563 _entityCache[key] = item; |
597 } | 564 } |
598 | 565 |
599 return item; | 566 return item; |
600 } | 567 } |
601 | 568 |
602 /** | 569 /// Return a list of values that are present for a dimension field. |
603 * Return a list of values that are present for a dimension field. | |
604 */ | |
605 List valuesForDimension(dynamic dimensionFieldName) { | 570 List valuesForDimension(dynamic dimensionFieldName) { |
606 int di = _dimFields.indexOf(dimensionFieldName); | 571 int di = _dimFields.indexOf(dimensionFieldName); |
607 if (di < 0) { | 572 if (di < 0) { |
608 return null; | 573 return null; |
609 } | 574 } |
610 List values = new List.from(_dimToIntMap[di].keys); | 575 List values = new List.from(_dimToIntMap[di].keys); |
611 values.sort( | 576 values.sort( |
612 comparators != null && comparators.containsKey(dimensionFieldName) ? | 577 comparators != null && comparators.containsKey(dimensionFieldName) |
613 comparators[dimensionFieldName] : _defaultDimComparator); | 578 ? comparators[dimensionFieldName] |
| 579 : _defaultDimComparator); |
614 return values; | 580 return values; |
615 } | 581 } |
616 } | 582 } |
617 | 583 |
618 /** | 584 /// Parse a path for nested map-like objects. |
619 * Parse a path for nested map-like objects. | 585 /// Caches the parsed key in the passed map. |
620 * Caches the parsed key in the passed map. | 586 /// |
621 * | 587 /// Takes map keys of the format: |
622 * Takes map keys of the format: | 588 /// "list(key=val;val=m).another(state=good).state" |
623 * "list(key=val;val=m).another(state=good).state" | 589 /// and outputs: |
624 * and outputs: | 590 /// ["list", {"key": "val", "val": "m"}, |
625 * ["list", {"key": "val", "val": "m"}, | 591 /// "another", {"state": "good"}, "state"] |
626 * "another", {"state": "good"}, "state"] | |
627 */ | |
628 List _parseKey(String key, Map parsedKeysCache) { | 592 List _parseKey(String key, Map parsedKeysCache) { |
629 List parts = parsedKeysCache == null ? null : parsedKeysCache[key]; | 593 List parts = parsedKeysCache == null ? null : parsedKeysCache[key]; |
630 if (parts == null && key != null) { | 594 if (parts == null && key != null) { |
631 parts = new List(); | 595 parts = new List(); |
632 if (key.contains(').')) { | 596 if (key.contains(').')) { |
633 int start = 0; | 597 int start = 0; |
634 int cursor = 0; | 598 int cursor = 0; |
635 bool inParams = false; | 599 bool inParams = false; |
636 List matchKeyVals; | 600 List matchKeyVals; |
637 Map listMatchingMap = {}; | 601 Map listMatchingMap = {}; |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
671 parts = key.split('.'); | 635 parts = key.split('.'); |
672 } | 636 } |
673 if (parsedKeysCache != null) { | 637 if (parsedKeysCache != null) { |
674 parsedKeysCache[key] = parts; | 638 parsedKeysCache[key] = parts; |
675 } | 639 } |
676 } | 640 } |
677 | 641 |
678 return parts; | 642 return parts; |
679 } | 643 } |
680 | 644 |
681 /** | 645 /// Walk a map-like structure that could have list in the path. |
682 * Walk a map-like structure that could have list in the path. | 646 /// |
683 * | 647 /// Example: |
684 * Example: | 648 /// Map testMap = { |
685 * Map testMap = { | 649 /// "first": "firstval", |
686 * "first": "firstval", | 650 /// "list": [ |
687 * "list": [ | 651 /// { "val": "m", |
688 * { "val": "m", | 652 /// "key": "val", |
689 * "key": "val", | 653 /// "another": [ |
690 * "another": [ | 654 /// { 'state': 'good' }, |
691 * { 'state': 'good' }, | 655 /// { 'state': 'bad' } |
692 * { 'state': 'bad' } | 656 /// ] |
693 * ] | 657 /// }, |
694 * }, | 658 /// { "val": "m", "key": "invalid" }, |
695 * { "val": "m", "key": "invalid" }, | 659 /// { "val": "o" } |
696 * { "val": "o" } | 660 /// ] |
697 * ] | 661 /// }; |
698 * }; | 662 /// |
699 * | 663 /// For the above map: |
700 * For the above map: | 664 /// walk(testMap, "list(key=val;val=m).another(state=good).state"); |
701 * walk(testMap, "list(key=val;val=m).another(state=good).state"); | 665 /// outputs: |
702 * outputs: | 666 /// good |
703 * good | |
704 */ | |
705 dynamic walk(initial, String key, Map parsedKeyCache) { | 667 dynamic walk(initial, String key, Map parsedKeyCache) { |
706 List parts = _parseKey(key, parsedKeyCache); | 668 List parts = _parseKey(key, parsedKeyCache); |
707 return parts.fold(initial, (current, part) { | 669 return parts.fold(initial, (current, part) { |
708 if (current == null) { | 670 if (current == null) { |
709 return null; | 671 return null; |
710 } else if (current is List && part is Map) { | 672 } else if (current is List && part is Map) { |
711 for (int i = 0; i < current.length; i++) { | 673 for (int i = 0; i < current.length; i++) { |
712 bool match = true; | 674 bool match = true; |
713 part.forEach((key, val) { | 675 part.forEach((key, val) { |
714 if ((key.contains('.') && | 676 if ((key.contains('.') && |
715 walk(part, key, parsedKeyCache).toString() != val) || | 677 walk(part, key, parsedKeyCache).toString() != val) || |
716 part[key] != val) { | 678 part[key] != val) { |
717 match = false; | 679 match = false; |
718 } | 680 } |
719 }); | 681 }); |
720 if (match) { | 682 if (match) { |
721 return current[i]; | 683 return current[i]; |
722 } | 684 } |
723 } | 685 } |
724 } else { | 686 } else { |
725 return current[part]; | 687 return current[part]; |
726 } | 688 } |
727 }); | 689 }); |
728 } | 690 } |
OLD | NEW |