/******************************************************************************* * Copyright (c) 2011, 2014 Ericsson * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Patrick Tasse - Initial API and implementation * Bernd Hufmann - Add support for event collapsing ******************************************************************************/ package org.eclipse.tracecompass.tmf.ui.viewers.events; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.tracecompass.internal.tmf.core.filter.TmfCollapseFilter; import org.eclipse.tracecompass.internal.tmf.ui.Activator; import org.eclipse.tracecompass.tmf.core.component.ITmfEventProvider; import org.eclipse.tracecompass.tmf.core.event.ITmfEvent; import org.eclipse.tracecompass.tmf.core.event.ITmfEventField; import org.eclipse.tracecompass.tmf.core.event.ITmfEventType; import org.eclipse.tracecompass.tmf.core.filter.ITmfFilter; import org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest; import org.eclipse.tracecompass.tmf.core.request.TmfEventRequest; import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp; import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange; import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; /** * The generic TMF Events table events cache * * This can help avoid re-reading the trace when the user scrolls a window, * for example. * * @author Patrick Tasse */ public class TmfEventsCache { /** * The generic TMF Events table cached event. * * @author Patrick Tasse */ public static class CachedEvent implements ITmfEvent { /** * Event reference. * * When {@link TmfCollapseFilter} is active then it's event reference * of the first event of repeated events. */ ITmfEvent event; /** * Events rank. * * When {@link TmfCollapseFilter} is active then it's event rank of the * first event of repeated events. */ long rank; /** * Number times event is repeated. Updated by using {@link TmfCollapseFilter} */ long repeatCount; /** * Constructor for new cached events. * * @param iTmfEvent * The original trace event * @param rank * The rank of this event in the trace */ public CachedEvent (ITmfEvent iTmfEvent, long rank) { this.event = iTmfEvent; this.rank = rank; } @Override public <T> T getAdapter(Class<T> adapterType) { return event.getAdapter(adapterType); } @Override public ITmfTrace getTrace() { return event.getTrace(); } @Override public long getRank() { return event.getRank(); } /** * @since 1.0 */ @Override public String getName() { return event.getName(); } @Override public ITmfTimestamp getTimestamp() { return event.getTimestamp(); } @Override public ITmfEventType getType() { return event.getType(); } @Override public ITmfEventField getContent() { return event.getContent(); } } private final CachedEvent[] fCache; private final int fCacheSize; private int fCacheStartIndex = 0; private int fCacheEndIndex = 0; private ITmfTrace fTrace; private final TmfEventsTable fTable; private ITmfFilter fFilter; private final List<Integer> fFilterIndex = new ArrayList<>(); // contains the event rank at each 'cache size' filtered events private boolean fCollapseFilterEnabled = false; /** * Constructor for the event cache * * @param cacheSize * The size of the cache, in number of events * @param table * The Events table this cache will cover */ public TmfEventsCache(int cacheSize, TmfEventsTable table) { fCacheSize = cacheSize; fCache = new CachedEvent[cacheSize * 2]; // the cache holds two blocks of cache size fTable = table; } /** * Assign a new trace to this events cache. This clears the current * contents. * * @param trace * The trace to assign. */ public void setTrace(ITmfTrace trace) { fTrace = trace; clear(); } /** * Clear the current contents of this cache. */ public synchronized void clear() { if (job != null && job.getState() != Job.NONE) { job.cancel(); } Arrays.fill(fCache, null); fCacheStartIndex = 0; fCacheEndIndex = 0; fFilterIndex.clear(); } /** * Apply a filter on this event cache. This clears the current cache * contents. * * @param filter * The ITmfFilter to apply. * @param collapseFilterEnabled * true if the collapse filter is enabled * @since 2.0 */ public void applyFilter(ITmfFilter filter, boolean collapseFilterEnabled) { fFilter = filter; fCollapseFilterEnabled = collapseFilterEnabled; clear(); } /** * Clear the current filter on this cache. This also clears the current * cache contents. */ public void clearFilter() { fFilter = null; fCollapseFilterEnabled = false; clear(); } /** * Get an event from the cache. If the cache does not contain the event, * a cache population request is triggered. * * @param index * The index of this event in the cache * @return The cached event, or 'null' if the event is not in the cache */ public synchronized CachedEvent getEvent(int index) { if ((index >= fCacheStartIndex) && (index < fCacheEndIndex)) { int i = index - fCacheStartIndex; return fCache[i]; } populateCache(index); return null; } /** * Peek an event in the cache. Does not trigger cache population. * * @param index * Index of the event to peek * @return The cached event, or 'null' if the event is not in the cache */ public synchronized CachedEvent peekEvent(int index) { if ((index >= fCacheStartIndex) && (index < fCacheEndIndex)) { int i = index - fCacheStartIndex; return fCache[i]; } return null; } /** * Add a trace event to the cache. * * @param event * The original trace event to be cached * @param rank * The rank of this event in the trace * @param index * The index this event will occupy in the cache */ public synchronized void storeEvent(ITmfEvent event, long rank, int index) { if (index == fCacheEndIndex) { int i = index - fCacheStartIndex; if (i < fCache.length) { fCache[i] = new CachedEvent(event, rank); fCacheEndIndex++; } } if ((fFilter != null) && ((index % fCacheSize) == 0)) { int i = index / fCacheSize; fFilterIndex.add(i, Integer.valueOf((int) rank)); } } /** * Update event repeat count at index * * @param index * The index this event occupies in the cache */ public synchronized void updateCollapsedEvent(int index) { int i = index - fCacheStartIndex; if (i < fCache.length) { fCache[i].repeatCount++; } } /** * Get the cache index of an event from his rank in the trace. This will * take in consideration any filter that might be applied. * * @param rank * The rank of the event in the trace * @return The position (index) this event should use once cached */ public int getFilteredEventIndex(final long rank) { int current; int startRank; TmfEventRequest request; final ITmfFilter filter = fFilter; synchronized (this) { int start = 0; int end = fFilterIndex.size(); if ((fCacheEndIndex - fCacheStartIndex) > 1) { if (rank < fCache[0].rank) { end = (fCacheStartIndex / fCacheSize) + 1; } else if (rank > fCache[fCacheEndIndex - fCacheStartIndex - 1].rank) { start = fCacheEndIndex / fCacheSize; } else { for (int i = 0; i < (fCacheEndIndex - fCacheStartIndex); i++) { if (fCache[i].rank >= rank) { return fCacheStartIndex + i; } } return fCacheEndIndex; } } current = (start + end) / 2; while (current != start) { if (rank < fFilterIndex.get(current)) { end = current; current = (start + end) / 2; } else { start = current; current = (start + end) / 2; } } startRank = fFilterIndex.size() > 0 ? fFilterIndex.get(current) : 0; } final int index = current * fCacheSize; class DataRequest extends TmfEventRequest { ITmfFilter requestFilter; TmfCollapseFilter requestCollapsedFilter; int requestRank; int requestIndex; DataRequest(Class<? extends ITmfEvent> dataType, ITmfFilter reqFilter, int start, int nbRequested) { super(dataType, TmfTimeRange.ETERNITY, start, nbRequested, TmfEventRequest.ExecutionType.FOREGROUND); requestFilter = reqFilter; requestRank = start; requestIndex = index; requestCollapsedFilter = fCollapseFilterEnabled ? new TmfCollapseFilter() : null; } @Override public void handleData(ITmfEvent event) { super.handleData(event); if (isCancelled()) { return; } if (requestRank >= rank) { cancel(); return; } requestRank++; if (requestFilter.matches(event)) { if (requestCollapsedFilter == null || requestCollapsedFilter.matches(event)) { requestIndex++; } } } public int getFilteredIndex() { return requestIndex; } } request = new DataRequest(ITmfEvent.class, filter, startRank, ITmfEventRequest.ALL_DATA); ((ITmfEventProvider) fTrace).sendRequest(request); try { request.waitForCompletion(); return ((DataRequest) request).getFilteredIndex(); } catch (InterruptedException e) { Activator.getDefault().logError("Filter request interrupted!", e); //$NON-NLS-1$ } return 0; } // ------------------------------------------------------------------------ // Event cache population // ------------------------------------------------------------------------ // The event fetching job private Job job; private synchronized void populateCache(final int index) { /* Check if the current job will fetch the requested event: * 1. The job must exist * 2. It must be running (i.e. not completed) * 3. The requested index must be within the cache range * * If the job meets these conditions, we simply exit. * Otherwise, we create a new job but we might have to cancel * an existing job for an obsolete range. */ if (job != null) { if (job.getState() != Job.NONE) { if ((index >= fCacheStartIndex) && (index < (fCacheStartIndex + fCache.length))) { return; } // The new index is out of the requested range // Kill the job and start a new one job.cancel(); } } // Populate the cache starting at the index that is one block less // of cache size than the requested index. The cache will hold two // consecutive blocks of cache size, centered on the requested index. fCacheStartIndex = Math.max(0, index - fCacheSize); fCacheEndIndex = fCacheStartIndex; job = new Job("Fetching Events") { //$NON-NLS-1$ private int startIndex = fCacheStartIndex; private int skipCount = 0; @Override protected IStatus run(final IProgressMonitor monitor) { int nbRequested; if (fFilter == null) { nbRequested = fCache.length; } else { nbRequested = ITmfEventRequest.ALL_DATA; int i = startIndex / fCacheSize; if (i < fFilterIndex.size()) { skipCount = startIndex - (i * fCacheSize); startIndex = fFilterIndex.get(i); } } TmfEventRequest request = new TmfEventRequest(ITmfEvent.class, TmfTimeRange.ETERNITY, startIndex, nbRequested, TmfEventRequest.ExecutionType.FOREGROUND) { private int count = 0; private long rank = startIndex; private TmfCollapseFilter collapseFilter = fCollapseFilterEnabled ? new TmfCollapseFilter() : null; @Override public void handleData(ITmfEvent event) { // If the job is canceled, cancel the request so waitForCompletion() will unlock if (monitor.isCanceled()) { cancel(); return; } super.handleData(event); if ((fFilter == null) || fFilter.matches(event)) { if (collapseFilter == null || collapseFilter.matches(event)) { if (skipCount-- <= 0) { synchronized (TmfEventsCache.this) { if (monitor.isCanceled()) { return; } fCache[count] = new CachedEvent(event, rank); count++; fCacheEndIndex++; } if (fFilter != null) { fTable.cacheUpdated(false); } } } else if ((count > 0) && (skipCount <= 0)) { fCache[count - 1].repeatCount++; } } if (count >= fCache.length) { cancel(); } else if ((fFilter != null) && (count >= (fTable.getTable().getItemCount() - 3))) { // -1 for header row, -2 for top and bottom filter status rows cancel(); } rank++; } }; ((ITmfEventProvider) fTrace).sendRequest(request); try { request.waitForCompletion(); } catch (InterruptedException e) { Activator.getDefault().logError("Wait for completion interrupted for populateCache ", e); //$NON-NLS-1$ } fTable.cacheUpdated(true); // Flag the UI thread that the cache is ready if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } return Status.OK_STATUS; } }; //job.setSystem(true); job.setPriority(Job.SHORT); job.schedule(); } }