| 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 |