java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/FixLoaderAndTracker.java
... ...
@@ -210,158 +210,159 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
210 210
Iterable<Timed> fixes, boolean returnManeuverChanges, boolean returnLiveDelay) {
211 211
final Set<RegattaAndRaceIdentifier> maneuverChanged = new HashSet<>();
212 212
final Map<RegattaAndRaceIdentifier, Duration> delayToLive = new HashMap<>();
213
- // TODO bug6236: how to improve performance of device mappings look-up when we have received multiple fixes from the same device?
214 213
if (!preemptiveStopRequested.get() && trackedRace.getStartOfTracking() != null) {
215
- final TimePoint timePoint = fix.getTimePoint();
216
- deviceMappings.forEachMappingOfDeviceIncludingTimePoint(device, fix.getTimePoint(),
217
- new Consumer<DeviceMappingWithRegattaLogEvent<WithID>>() {
218
- @Override
219
- public void accept(DeviceMappingWithRegattaLogEvent<WithID> mapping) {
220
- mapping.getRegattaLogEvent().accept(new MappingEventVisitor() {
221
- @Override
222
- public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
223
- recordSensorFixForCompetitor(event.getMappedTo(), event);
224
- }
225
-
226
- @Override
227
- public void visit(RegattaLogDeviceBoatSensorDataMappingEvent event) {
228
- final Boat boat = event.getMappedTo();
229
- final Competitor competitor = trackedRace.getCompetitorOfBoat(boat);
230
- if (competitor != null) {
231
- recordSensorFixForCompetitor(competitor, event);
232
- } else {
233
- logger.log(Level.FINE, ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
214
+ for (final Timed fix : fixes) {
215
+ final TimePoint timePoint = fix.getTimePoint();
216
+ deviceMappings.forEachMappingOfDeviceIncludingTimePoint(device, fix.getTimePoint(),
217
+ new Consumer<DeviceMappingWithRegattaLogEvent<WithID>>() {
218
+ @Override
219
+ public void accept(DeviceMappingWithRegattaLogEvent<WithID> mapping) {
220
+ mapping.getRegattaLogEvent().accept(new MappingEventVisitor() {
221
+ @Override
222
+ public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
223
+ recordSensorFixForCompetitor(event.getMappedTo(), event);
234 224
}
235
- }
236
-
237
- private void recordSensorFixForCompetitor(Competitor competitor, RegattaLogDeviceMappingEvent<?> event) {
238
- if (!preemptiveStopRequested.get()) {
239
- @SuppressWarnings("unchecked")
240
- SensorFixMapper<SensorFix, DynamicSensorFixTrack<Competitor, SensorFix>, Competitor> mapper = sensorFixMapperFactory
241
- .createCompetitorMapper((Class<? extends RegattaLogDeviceMappingEvent<?>>) event.getClass());
242
- DynamicSensorFixTrack<Competitor, SensorFix> track = mapper.getTrack(trackedRace, competitor);
243
- if (track != null && trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
244
- mapper.addFix(track, (DoubleVectorFix) fix);
245
- if (returnLiveDelay) {
246
- delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
247
- }
225
+
226
+ @Override
227
+ public void visit(RegattaLogDeviceBoatSensorDataMappingEvent event) {
228
+ final Boat boat = event.getMappedTo();
229
+ final Competitor competitor = trackedRace.getCompetitorOfBoat(boat);
230
+ if (competitor != null) {
231
+ recordSensorFixForCompetitor(competitor, event);
232
+ } else {
233
+ logger.log(Level.FINE, ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
248 234
}
249 235
}
250
- }
251
-
252
- @Override
253
- public void visit(RegattaLogDeviceCompetitorMappingEvent event) {
254
- recordForCompetitor(event.getMappedTo());
255
- }
256
-
257
- @Override
258
- public void visit(RegattaLogDeviceBoatMappingEvent event) {
259
- final Boat boat = event.getMappedTo();
260
- final Competitor comp = trackedRace.getCompetitorOfBoat(boat);
261
- if (comp != null) {
262
- recordForCompetitor(comp);
263
- } else {
264
- // this is not necessarily something to warn of; while a boat tracker may continuously track
265
- logger.log(Level.FINE,
266
- ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
267
- }
268
- }
269
-
270
- private void recordForCompetitor(Competitor comp) {
271
- if (!preemptiveStopRequested.get()) {
272
- if (fix instanceof GPSFixMoving) {
273
- // try to record the fix, and only if it was really to the track,
274
- // check for maneuvers; otherwise, the fix may not have been accepted
275
- // by the race or the track, e.g., because the race's end-of-tracking
276
- // comes before the fix's time point
277
- if (trackedRace.recordFix(comp, (GPSFixMoving) fix)) { // TOOD bug6229: this checks the TrackedRace's tracking interval, but for an MDI we'd also want to intersect with Event/Regatta end date if set
278
- if (returnManeuverChanges) {
279
- RegattaAndRaceIdentifier maneuverChangedAnswer = detectIfManeuverChanged(comp);
280
- if (maneuverChangedAnswer != null) {
281
- maneuverChanged.add(maneuverChangedAnswer);
282
- }
283
- }
236
+
237
+ private void recordSensorFixForCompetitor(Competitor competitor, RegattaLogDeviceMappingEvent<?> event) {
238
+ if (!preemptiveStopRequested.get()) {
239
+ @SuppressWarnings("unchecked")
240
+ SensorFixMapper<SensorFix, DynamicSensorFixTrack<Competitor, SensorFix>, Competitor> mapper = sensorFixMapperFactory
241
+ .createCompetitorMapper((Class<? extends RegattaLogDeviceMappingEvent<?>>) event.getClass());
242
+ DynamicSensorFixTrack<Competitor, SensorFix> track = mapper.getTrack(trackedRace, competitor);
243
+ if (track != null && trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
244
+ mapper.addFix(track, (DoubleVectorFix) fix);
284 245
if (returnLiveDelay) {
285 246
delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
286 247
}
287 248
}
288
- } else {
289
- logger.log(Level.WARNING,
290
- String.format(
291
- "Could not add fix for competitor (%s) in race (%s), as it"
292
- + " is no GPSFixMoving, meaning it is missing COG/SOG values",
293
- comp, trackedRace.getRace().getName()));
294 249
}
295 250
}
296
- }
297
-
298
- @Override
299
- public void visit(RegattaLogDeviceMarkMappingEvent event) {
300
- if (!preemptiveStopRequested.get()) {
301
- Mark mark = event.getMappedTo();
302
- final DynamicGPSFixTrack<Mark, GPSFix> markTrack = trackedRace.getOrCreateTrack(mark);
303
- final GPSFix firstFixAtOrAfter;
304
- final boolean forceFix;
305
- if (trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
306
- forceFix = false;
251
+
252
+ @Override
253
+ public void visit(RegattaLogDeviceCompetitorMappingEvent event) {
254
+ recordForCompetitor(event.getMappedTo());
255
+ }
256
+
257
+ @Override
258
+ public void visit(RegattaLogDeviceBoatMappingEvent event) {
259
+ final Boat boat = event.getMappedTo();
260
+ final Competitor comp = trackedRace.getCompetitorOfBoat(boat);
261
+ if (comp != null) {
262
+ recordForCompetitor(comp);
307 263
} else {
308
- markTrack.lockForRead();
309
- try {
310
- if (Util.isEmpty(markTrack.getRawFixes())
311
- || (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null
312
- && firstFixAtOrAfter.getTimePoint().equals(timePoint)) {
313
- // either the first fix or overwriting an existing one
314
- forceFix = true;
315
- } else {
316
- // checking if the given fix is "better" than an existing one
317
- TimePoint startOfTracking = trackedRace.getStartOfTracking();
318
- TimePoint endOfTracking = trackedRace.getEndOfTracking();
319
- if (startOfTracking != null) {
320
- GPSFix fixAfterStartOfTracking = markTrack
321
- .getFirstFixAtOrAfter(startOfTracking);
322
- if (fixAfterStartOfTracking == null
323
- || !trackedRace.isWithinStartAndEndOfTracking(
324
- fixAfterStartOfTracking.getTimePoint())) {
325
- // There is no fix in the tracking interval, so this fix could be "better"
326
- // than ones already available in the track
327
- // Better means closer before/after the beginning/end of the tracking
328
- // interval
329
- if (timePoint.before(startOfTracking)) {
330
- // check if it is closer to the beginning of the tracking interval
331
- GPSFix fixBeforeStartOfTracking = markTrack
332
- .getLastFixAtOrBefore(startOfTracking);
333
- forceFix = (fixBeforeStartOfTracking == null
334
- || fixBeforeStartOfTracking.getTimePoint().before(timePoint));
335
- } else if (endOfTracking != null && timePoint.after(endOfTracking)) {
336
- // check if it is closer to the end of the tracking interval
337
- GPSFix fixAfterEndOfTracking = markTrack
338
- .getFirstFixAtOrAfter(endOfTracking);
339
- forceFix = (fixAfterEndOfTracking == null
340
- || fixAfterEndOfTracking.getTimePoint().after(timePoint));
264
+ // this is not necessarily something to warn of; while a boat tracker may continuously track
265
+ logger.log(Level.FINE,
266
+ ()->"Could not record fix for boat because no competitor could be determined. Boat: " + boat);
267
+ }
268
+ }
269
+
270
+ private void recordForCompetitor(Competitor comp) {
271
+ if (!preemptiveStopRequested.get()) {
272
+ if (fix instanceof GPSFixMoving) {
273
+ // try to record the fix, and only if it was really to the track,
274
+ // check for maneuvers; otherwise, the fix may not have been accepted
275
+ // by the race or the track, e.g., because the race's end-of-tracking
276
+ // comes before the fix's time point
277
+ if (trackedRace.recordFix(comp, (GPSFixMoving) fix)) { // TOOD bug6229: this checks the TrackedRace's tracking interval, but for an MDI we'd also want to intersect with Event/Regatta end date if set
278
+ if (returnManeuverChanges) {
279
+ RegattaAndRaceIdentifier maneuverChangedAnswer = detectIfManeuverChanged(comp);
280
+ if (maneuverChangedAnswer != null) {
281
+ maneuverChanged.add(maneuverChangedAnswer);
282
+ }
283
+ }
284
+ if (returnLiveDelay) {
285
+ delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
286
+ }
287
+ }
288
+ } else {
289
+ logger.log(Level.WARNING,
290
+ String.format(
291
+ "Could not add fix for competitor (%s) in race (%s), as it"
292
+ + " is no GPSFixMoving, meaning it is missing COG/SOG values",
293
+ comp, trackedRace.getRace().getName()));
294
+ }
295
+ }
296
+ }
297
+
298
+ @Override
299
+ public void visit(RegattaLogDeviceMarkMappingEvent event) {
300
+ if (!preemptiveStopRequested.get()) {
301
+ Mark mark = event.getMappedTo();
302
+ final DynamicGPSFixTrack<Mark, GPSFix> markTrack = trackedRace.getOrCreateTrack(mark);
303
+ final GPSFix firstFixAtOrAfter;
304
+ final boolean forceFix;
305
+ if (trackedRace.isWithinStartAndEndOfTracking(fix.getTimePoint())) {
306
+ forceFix = false;
307
+ } else {
308
+ markTrack.lockForRead();
309
+ try {
310
+ if (Util.isEmpty(markTrack.getRawFixes())
311
+ || (firstFixAtOrAfter = markTrack.getFirstFixAtOrAfter(timePoint)) != null
312
+ && firstFixAtOrAfter.getTimePoint().equals(timePoint)) {
313
+ // either the first fix or overwriting an existing one
314
+ forceFix = true;
315
+ } else {
316
+ // checking if the given fix is "better" than an existing one
317
+ TimePoint startOfTracking = trackedRace.getStartOfTracking();
318
+ TimePoint endOfTracking = trackedRace.getEndOfTracking();
319
+ if (startOfTracking != null) {
320
+ GPSFix fixAfterStartOfTracking = markTrack
321
+ .getFirstFixAtOrAfter(startOfTracking);
322
+ if (fixAfterStartOfTracking == null
323
+ || !trackedRace.isWithinStartAndEndOfTracking(
324
+ fixAfterStartOfTracking.getTimePoint())) {
325
+ // There is no fix in the tracking interval, so this fix could be "better"
326
+ // than ones already available in the track
327
+ // Better means closer before/after the beginning/end of the tracking
328
+ // interval
329
+ if (timePoint.before(startOfTracking)) {
330
+ // check if it is closer to the beginning of the tracking interval
331
+ GPSFix fixBeforeStartOfTracking = markTrack
332
+ .getLastFixAtOrBefore(startOfTracking);
333
+ forceFix = (fixBeforeStartOfTracking == null
334
+ || fixBeforeStartOfTracking.getTimePoint().before(timePoint));
335
+ } else if (endOfTracking != null && timePoint.after(endOfTracking)) {
336
+ // check if it is closer to the end of the tracking interval
337
+ GPSFix fixAfterEndOfTracking = markTrack
338
+ .getFirstFixAtOrAfter(endOfTracking);
339
+ forceFix = (fixAfterEndOfTracking == null
340
+ || fixAfterEndOfTracking.getTimePoint().after(timePoint));
341
+ } else {
342
+ forceFix = false;
343
+ }
341 344
} else {
345
+ // there is already a fix in the tracking interval
342 346
forceFix = false;
343 347
}
344 348
} else {
345
- // there is already a fix in the tracking interval
346 349
forceFix = false;
347 350
}
348
- } else {
349
- forceFix = false;
350 351
}
352
+ } finally {
353
+ markTrack.unlockAfterRead();
351 354
}
352
- } finally {
353
- markTrack.unlockAfterRead();
354 355
}
355
- }
356
- trackedRace.recordFix(mark, (GPSFix) fix, /* only when in tracking interval */ !forceFix);
357
- if (returnLiveDelay) {
358
- delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
356
+ trackedRace.recordFix(mark, (GPSFix) fix, /* only when in tracking interval */ !forceFix);
357
+ if (returnLiveDelay) {
358
+ delayToLive.put(trackedRace.getRaceIdentifier(), new MillisecondsDurationImpl(trackedRace.getDelayToLiveInMillis()));
359
+ }
359 360
}
360 361
}
361
- }
362
- });
363
- }
364
- });
362
+ });
363
+ }
364
+ });
365
+ }
365 366
}
366 367
return mergeManeuverChangedAndLiveDelayResult(maneuverChanged, delayToLive);
367 368
}
... ...
@@ -879,6 +880,11 @@ public class FixLoaderAndTracker implements TrackingDataLoader {
879 880
addLoadingJob(new LoadFixesForNewlyCoveredTimeRangesJob(item, newlyCoveredTimeRanges));
880 881
}
881 882
}
883
+
884
+ @Override
885
+ public String toString() {
886
+ return "FixLoaderDeviceMappings for race "+trackedRace.getRaceIdentifier();
887
+ }
882 888
}
883 889
884 890
/**
java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/fixtracker/RegattaLogDeviceMappings.java
... ...
@@ -4,6 +4,7 @@ import java.util.ArrayList;
4 4
import java.util.Collections;
5 5
import java.util.HashMap;
6 6
import java.util.HashSet;
7
+import java.util.LinkedList;
7 8
import java.util.List;
8 9
import java.util.Map;
9 10
import java.util.Set;
... ...
@@ -69,6 +70,31 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
69 70
private final Map<ItemT, List<DeviceMappingWithRegattaLogEvent<ItemT>>> mappings = new HashMap<>();
70 71
private final Map<DeviceIdentifier, List<DeviceMappingWithRegattaLogEvent<ItemT>>> mappingsByDevice = new HashMap<>();
71 72
73
+ /**
74
+ * A cache that holds the device mappings as the {@link Pair#getB() second} component of the values in this map,
75
+ * such that exactly these device mappings apply for any time point {@link TimeRange#includes(TimePoint) included}
76
+ * by the {@link TimeRange} that is the {@link Pair#getA() first} component of a value in this map. This map's keys
77
+ * match with the {@link DeviceMapping#getDevice() device identifiers} of the {@link Pair#getB() second} components
78
+ * of their corresponding values.
79
+ * <p>
80
+ *
81
+ * This cache is designed to work well for cases where mappings change at a frequency orders of magnitude less than
82
+ * the frequency with which fixes arrive and are to be mapped to items. Furthermore, the cache hit rates benefit
83
+ * from mappings covering large time ranges.
84
+ * <p>
85
+ *
86
+ * Any change to the mappings for a device will remove the mapping for the device's {@link DeviceIdentifier
87
+ * identifier} from this map.
88
+ * <p>
89
+ *
90
+ * Access to this map has to undergo the same locking drill as any access to {@link #mappings}, using the
91
+ * {@link #mappingsLock}.
92
+ */
93
+ private final Map<DeviceIdentifier, Pair<TimeRange, List<DeviceMappingWithRegattaLogEvent<ItemT>>>> cachedMappings = new HashMap<>();
94
+
95
+ private int cacheHits;
96
+ private int cacheMisses;
97
+
72 98
private final RegattaLogEventVisitor regattaLogEventVisitor = new BaseRegattaLogEventVisitor() {
73 99
@Override
74 100
public void visit(RegattaLogDeviceCompetitorSensorDataMappingEvent event) {
... ...
@@ -169,13 +195,38 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
169 195
public void forEachMappingOfDeviceIncludingTimePoint(DeviceIdentifier device, TimePoint timePoint,
170 196
Consumer<DeviceMappingWithRegattaLogEvent<ItemT>> callback) {
171 197
LockUtil.executeWithReadLock(mappingsLock, () -> {
172
- List<DeviceMappingWithRegattaLogEvent<ItemT>> mappingsForDevice = mappingsByDevice.get(device);
173
- if (mappingsForDevice != null) {
174
- for (DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
175
- if (mapping.getTimeRange().includes(timePoint)) {
176
- callback.accept(mapping);
198
+ final Pair<TimeRange, List<DeviceMappingWithRegattaLogEvent<ItemT>>> cachedTimeRangeForDevice = cachedMappings.get(device);
199
+ if (cachedTimeRangeForDevice != null && cachedTimeRangeForDevice.getA().includes(timePoint)) {
200
+ cacheHits++;
201
+ logger.fine(() -> "Device mapping cache hit for mapper " + this + " for device " + device
202
+ + " and time point " + timePoint + ", included in cached range "
203
+ + cachedTimeRangeForDevice.getA() + "; " + cacheHits + " hits, " + cacheMisses + " misses");
204
+ cachedTimeRangeForDevice.getB().forEach(mapping->callback.accept(mapping));
205
+ } else {
206
+ final List<DeviceMappingWithRegattaLogEvent<ItemT>> mappingsForDevice = mappingsByDevice.get(device);
207
+ MultiTimeRange timeRangeForCache = MultiTimeRange.of();
208
+ final List<DeviceMappingWithRegattaLogEvent<ItemT>> deviceMappingsForCache = new LinkedList<>();
209
+ assert timeRangeForCache.isEmpty();
210
+ cacheMisses++;
211
+ if (mappingsForDevice != null) {
212
+ for (final DeviceMappingWithRegattaLogEvent<ItemT> mapping : mappingsForDevice) {
213
+ if (mapping.getTimeRange().includes(timePoint)) {
214
+ if (timeRangeForCache.isEmpty()) {
215
+ timeRangeForCache.union(mapping.getTimeRange());
216
+ } else {
217
+ timeRangeForCache = timeRangeForCache.intersection(mapping.getTimeRange());
218
+ }
219
+ deviceMappingsForCache.add(mapping);
220
+ callback.accept(mapping);
221
+ }
177 222
}
178 223
}
224
+ final MultiTimeRange finalTimeRangeForache = timeRangeForCache;
225
+ logger.fine(() -> "Device mapping cache miss for mapper " + this + " for device " + device
226
+ + " and time point " + timePoint + ", determined cachable range "
227
+ + finalTimeRangeForache + "; " + cacheHits + " hits, " + cacheMisses + " misses");
228
+
229
+ // TODO bug6236 Now issue the caching of the entry (device, (timeRangeForCache, deviceMappingsForCache)) under the mappingsLock's write lock!
179 230
}
180 231
});
181 232
}
... ...
@@ -245,6 +296,7 @@ public abstract class RegattaLogDeviceMappings<ItemT extends WithID> {
245 296
oldMappings.putAll(mappings);
246 297
oldDeviceIds.addAll(mappingsByDevice.keySet());
247 298
mappings.clear();
299
+ cachedMappings.clear();
248 300
mappings.putAll(newMappings);
249 301
mappingsByDevice.clear();
250 302
for (ItemT item : newMappings.keySet()) {