tmf: remove deprecated methods from tmf
[deliverable/tracecompass.git] / tmf / org.eclipse.tracecompass.tmf.core / src / org / eclipse / tracecompass / tmf / core / parsers / custom / CustomTxtTrace.java
1 /*******************************************************************************
2 * Copyright (c) 2010, 2016 Ericsson
3 *
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Patrick Tasse - Initial API and implementation
11 * Bernd Hufmann - Add trace type id handling
12 *******************************************************************************/
13
14 package org.eclipse.tracecompass.tmf.core.parsers.custom;
15
16 import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
17
18 import java.io.File;
19 import java.io.FileNotFoundException;
20 import java.io.IOException;
21 import java.nio.ByteBuffer;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map.Entry;
26 import java.util.regex.Matcher;
27
28 import org.eclipse.core.resources.IProject;
29 import org.eclipse.core.resources.IResource;
30 import org.eclipse.core.runtime.IStatus;
31 import org.eclipse.core.runtime.Status;
32 import org.eclipse.jdt.annotation.NonNull;
33 import org.eclipse.tracecompass.internal.tmf.core.Activator;
34 import org.eclipse.tracecompass.internal.tmf.core.parsers.custom.CustomEventAspects;
35 import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
36 import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
37 import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
38 import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
39 import org.eclipse.tracecompass.tmf.core.io.BufferedRandomAccessFile;
40 import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTraceDefinition.InputLine;
41 import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
42 import org.eclipse.tracecompass.tmf.core.signal.TmfTraceRangeUpdatedSignal;
43 import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
44 import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
45 import org.eclipse.tracecompass.tmf.core.trace.ITmfContext;
46 import org.eclipse.tracecompass.tmf.core.trace.TmfContext;
47 import org.eclipse.tracecompass.tmf.core.trace.TmfTrace;
48 import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
49 import org.eclipse.tracecompass.tmf.core.trace.TraceValidationStatus;
50 import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfPersistentlyIndexable;
51 import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfTraceIndexer;
52 import org.eclipse.tracecompass.tmf.core.trace.indexer.TmfBTreeTraceIndexer;
53 import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.ITmfCheckpoint;
54 import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.TmfCheckpoint;
55 import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
56 import org.eclipse.tracecompass.tmf.core.trace.location.TmfLongLocation;
57
58 /**
59 * Base class for custom plain text traces.
60 *
61 * @author Patrick Tassé
62 */
63 public class CustomTxtTrace extends TmfTrace implements ITmfPersistentlyIndexable {
64
65 private static final TmfLongLocation NULL_LOCATION = new TmfLongLocation(-1L);
66 private static final int DEFAULT_CACHE_SIZE = 100;
67 private static final int MAX_LINES = 100;
68 private static final int MAX_CONFIDENCE = 100;
69
70 private final CustomTxtTraceDefinition fDefinition;
71 private final ITmfEventField fRootField;
72 private BufferedRandomAccessFile fFile;
73 private final @NonNull String fTraceTypeId;
74
75 private static final char SEPARATOR = ':';
76 private static final String CUSTOM_TXT_TRACE_TYPE_PREFIX = "custom.txt.trace" + SEPARATOR; //$NON-NLS-1$
77 private static final String LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX = "org.eclipse.linuxtools.tmf.core.parsers.custom.CustomTxtTrace" + SEPARATOR; //$NON-NLS-1$
78 private static final String EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX = "org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTrace" + SEPARATOR; //$NON-NLS-1$
79
80 /**
81 * Basic constructor.
82 *
83 * @param definition
84 * Text trace definition
85 */
86 public CustomTxtTrace(final CustomTxtTraceDefinition definition) {
87 fDefinition = definition;
88 fRootField = CustomEventType.getRootField(definition);
89 fTraceTypeId = buildTraceTypeId(definition.categoryName, definition.definitionName);
90 setCacheSize(DEFAULT_CACHE_SIZE);
91 }
92
93 /**
94 * Full constructor.
95 *
96 * @param resource
97 * Trace's resource.
98 * @param definition
99 * Text trace definition
100 * @param path
101 * Path to the trace file
102 * @param cacheSize
103 * Cache size to use
104 * @throws TmfTraceException
105 * If we couldn't open the trace at 'path'
106 */
107 public CustomTxtTrace(final IResource resource,
108 final CustomTxtTraceDefinition definition, final String path,
109 final int cacheSize) throws TmfTraceException {
110 this(definition);
111 setCacheSize((cacheSize > 0) ? cacheSize : DEFAULT_CACHE_SIZE);
112 initTrace(resource, path, CustomTxtEvent.class);
113 }
114
115 @Override
116 public void initTrace(final IResource resource, final String path, final Class<? extends ITmfEvent> eventType) throws TmfTraceException {
117 super.initTrace(resource, path, eventType);
118 initFile();
119 }
120
121 private void initFile() throws TmfTraceException {
122 closeFile();
123 try {
124 fFile = new BufferedRandomAccessFile(getPath(), "r"); //$NON-NLS-1$
125 } catch (IOException e) {
126 throw new TmfTraceException(e.getMessage(), e);
127 }
128 }
129
130 @Override
131 public synchronized void dispose() {
132 super.dispose();
133 closeFile();
134 }
135
136 private void closeFile() {
137 if (fFile != null) {
138 try {
139 fFile.close();
140 } catch (IOException e) {
141 } finally {
142 fFile = null;
143 }
144 }
145 }
146
147 @Override
148 public ITmfTraceIndexer getIndexer() {
149 return super.getIndexer();
150 }
151
152 @Override
153 public Iterable<ITmfEventAspect<?>> getEventAspects() {
154 return CustomEventAspects.generateAspects(fDefinition);
155 }
156
157 @Override
158 public synchronized TmfContext seekEvent(final ITmfLocation location) {
159 final CustomTxtTraceContext context = new CustomTxtTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
160 if (NULL_LOCATION.equals(location) || fFile == null) {
161 return context;
162 }
163 try {
164 if (location == null) {
165 fFile.seek(0);
166 } else if (location.getLocationInfo() instanceof Long) {
167 fFile.seek((Long) location.getLocationInfo());
168 }
169 long rawPos = fFile.getFilePointer();
170 String line = fFile.getNextLine();
171 while (line != null) {
172 for (final InputLine input : getFirstLines()) {
173 final Matcher matcher = input.getPattern().matcher(line);
174 if (matcher.matches()) {
175 context.setLocation(new TmfLongLocation(rawPos));
176 context.firstLineMatcher = matcher;
177 context.firstLine = line;
178 context.nextLineLocation = fFile.getFilePointer();
179 context.inputLine = input;
180 return context;
181 }
182 }
183 rawPos = fFile.getFilePointer();
184 line = fFile.getNextLine();
185 }
186 return context;
187 } catch (final FileNotFoundException e) {
188 Activator.logError("Error seeking event. File not found: " + getPath(), e); //$NON-NLS-1$
189 return context;
190 } catch (final IOException e) {
191 Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
192 return context;
193 }
194
195 }
196
197 @Override
198 public synchronized TmfContext seekEvent(final double ratio) {
199 if (fFile == null) {
200 return new CustomTxtTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
201 }
202 try {
203 long pos = Math.round(ratio * fFile.length());
204 while (pos > 0) {
205 fFile.seek(pos - 1);
206 if (fFile.read() == '\n') {
207 break;
208 }
209 pos--;
210 }
211 final ITmfLocation location = new TmfLongLocation(pos);
212 final TmfContext context = seekEvent(location);
213 context.setRank(ITmfContext.UNKNOWN_RANK);
214 return context;
215 } catch (final IOException e) {
216 Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
217 return new CustomTxtTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
218 }
219 }
220
221 @Override
222 public synchronized double getLocationRatio(final ITmfLocation location) {
223 if (fFile == null) {
224 return 0;
225 }
226 try {
227 if (location.getLocationInfo() instanceof Long) {
228 return ((Long) location.getLocationInfo()).doubleValue() / fFile.length();
229 }
230 } catch (final IOException e) {
231 Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
232 }
233 return 0;
234 }
235
236 @Override
237 public ITmfLocation getCurrentLocation() {
238 // TODO Auto-generated method stub
239 return null;
240 }
241
242 @Override
243 public synchronized CustomTxtEvent parseEvent(final ITmfContext tmfContext) {
244 ITmfContext context = seekEvent(tmfContext.getLocation());
245 return parse(context);
246 }
247
248 @Override
249 public synchronized CustomTxtEvent getNext(final ITmfContext context) {
250 final ITmfContext savedContext = new TmfContext(context.getLocation(), context.getRank());
251 final CustomTxtEvent event = parse(context);
252 if (event != null) {
253 updateAttributes(savedContext, event);
254 context.increaseRank();
255 }
256 return event;
257 }
258
259 private synchronized CustomTxtEvent parse(final ITmfContext tmfContext) {
260 if (fFile == null) {
261 return null;
262 }
263 if (!(tmfContext instanceof CustomTxtTraceContext)) {
264 return null;
265 }
266
267 final CustomTxtTraceContext context = (CustomTxtTraceContext) tmfContext;
268 ITmfLocation location = context.getLocation();
269 if (location == null || !(location.getLocationInfo() instanceof Long) || NULL_LOCATION.equals(location)) {
270 return null;
271 }
272
273 CustomTxtEvent event = parseFirstLine(context);
274
275 final HashMap<InputLine, Integer> countMap = new HashMap<>();
276 InputLine currentInput = null;
277 if (context.inputLine.childrenInputs != null && context.inputLine.childrenInputs.size() > 0) {
278 currentInput = context.inputLine.childrenInputs.get(0);
279 countMap.put(currentInput, 0);
280 }
281
282 try {
283 if (fFile.getFilePointer() != context.nextLineLocation) {
284 fFile.seek(context.nextLineLocation);
285 }
286 long rawPos = fFile.getFilePointer();
287 String line = fFile.getNextLine();
288 while (line != null) {
289 boolean processed = false;
290 if (currentInput == null) {
291 for (final InputLine input : getFirstLines()) {
292 final Matcher matcher = input.getPattern().matcher(line);
293 if (matcher.matches()) {
294 context.setLocation(new TmfLongLocation(rawPos));
295 context.firstLineMatcher = matcher;
296 context.firstLine = line;
297 context.nextLineLocation = fFile.getFilePointer();
298 context.inputLine = input;
299 return event;
300 }
301 }
302 } else {
303 if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMinCount()) {
304 final List<InputLine> nextInputs = currentInput.getNextInputs(countMap);
305 if (nextInputs.size() == 0 || nextInputs.get(nextInputs.size() - 1).getMinCount() == 0) {
306 for (final InputLine input : getFirstLines()) {
307 final Matcher matcher = input.getPattern().matcher(line);
308 if (matcher.matches()) {
309 context.setLocation(new TmfLongLocation(rawPos));
310 context.firstLineMatcher = matcher;
311 context.firstLine = line;
312 context.nextLineLocation = fFile.getFilePointer();
313 context.inputLine = input;
314 return event;
315 }
316 }
317 }
318 for (final InputLine input : nextInputs) {
319 final Matcher matcher = input.getPattern().matcher(line);
320 if (matcher.matches()) {
321 event.processGroups(input, matcher);
322 currentInput = input;
323 if (countMap.get(currentInput) == null) {
324 countMap.put(currentInput, 1);
325 } else {
326 countMap.put(currentInput, checkNotNull(countMap.get(currentInput)) + 1);
327 }
328 Iterator<InputLine> iter = countMap.keySet().iterator();
329 while (iter.hasNext()) {
330 final InputLine inputLine = iter.next();
331 if (inputLine.level > currentInput.level) {
332 iter.remove();
333 }
334 }
335 if (currentInput.childrenInputs != null && currentInput.childrenInputs.size() > 0) {
336 currentInput = currentInput.childrenInputs.get(0);
337 countMap.put(currentInput, 0);
338 } else if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMaxCount()) {
339 if (currentInput.getNextInputs(countMap).size() > 0) {
340 currentInput = currentInput.getNextInputs(countMap).get(0);
341 if (countMap.get(currentInput) == null) {
342 countMap.put(currentInput, 0);
343 }
344 iter = countMap.keySet().iterator();
345 while (iter.hasNext()) {
346 final InputLine inputLine = iter.next();
347 if (inputLine.level > currentInput.level) {
348 iter.remove();
349 }
350 }
351 } else {
352 currentInput = null;
353 }
354 }
355 processed = true;
356 break;
357 }
358 }
359 }
360 if (!processed && currentInput != null) {
361 final Matcher matcher = currentInput.getPattern().matcher(line);
362 if (matcher.matches()) {
363 event.processGroups(currentInput, matcher);
364 countMap.put(currentInput, checkNotNull(countMap.get(currentInput)) + 1);
365 if (currentInput.childrenInputs != null && currentInput.childrenInputs.size() > 0) {
366 currentInput = currentInput.childrenInputs.get(0);
367 countMap.put(currentInput, 0);
368 } else if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMaxCount()) {
369 if (currentInput.getNextInputs(countMap).size() > 0) {
370 currentInput = currentInput.getNextInputs(countMap).get(0);
371 if (countMap.get(currentInput) == null) {
372 countMap.put(currentInput, 0);
373 }
374 final Iterator<InputLine> iter = countMap.keySet().iterator();
375 while (iter.hasNext()) {
376 final InputLine inputLine = iter.next();
377 if (inputLine.level > currentInput.level) {
378 iter.remove();
379 }
380 }
381 } else {
382 currentInput = null;
383 }
384 }
385 }
386 ((StringBuffer) event.getContentValue()).append("\n").append(line); //$NON-NLS-1$
387 }
388 }
389 rawPos = fFile.getFilePointer();
390 line = fFile.getNextLine();
391 }
392 } catch (final IOException e) {
393 Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
394 }
395 for (final Entry<InputLine, Integer> entry : countMap.entrySet()) {
396 if (entry.getValue() < entry.getKey().getMinCount()) {
397 event = null;
398 }
399 }
400 context.setLocation(NULL_LOCATION);
401 return event;
402 }
403
404 /**
405 * @return The first few lines of the text file
406 */
407 public List<InputLine> getFirstLines() {
408 return fDefinition.inputs;
409 }
410
411 /**
412 * Parse the first line of the trace (to recognize the type).
413 *
414 * @param context
415 * Trace context
416 * @return The first event
417 */
418 public CustomTxtEvent parseFirstLine(final CustomTxtTraceContext context) {
419 CustomTxtEventType eventType = new CustomTxtEventType(checkNotNull(fDefinition.definitionName), fRootField);
420 final CustomTxtEvent event = new CustomTxtEvent(fDefinition, this, TmfTimestamp.ZERO, eventType);
421 event.processGroups(context.inputLine, context.firstLineMatcher);
422 event.setContent(new CustomEventContent(event, new StringBuffer(context.firstLine)));
423 return event;
424 }
425
426 /**
427 * Get the trace definition.
428 *
429 * @return The trace definition
430 */
431 public CustomTraceDefinition getDefinition() {
432 return fDefinition;
433 }
434
435 /**
436 * {@inheritDoc}
437 * <p>
438 * The default implementation computes the confidence as the percentage of
439 * lines in the first 100 lines of the file which match any of the root
440 * input line patterns.
441 */
442 @Override
443 public IStatus validate(IProject project, String path) {
444 File file = new File(path);
445 if (!file.exists() || !file.isFile() || !file.canRead()) {
446 return new Status(IStatus.ERROR, Activator.PLUGIN_ID, Messages.CustomTrace_FileNotFound + ": " + path); //$NON-NLS-1$
447 }
448 int confidence = 0;
449 try {
450 if (!TmfTraceUtils.isText(file)) {
451 return new TraceValidationStatus(confidence, Activator.PLUGIN_ID);
452 }
453 } catch (IOException e) {
454 Activator.logError("Error validating file: " + path, e); //$NON-NLS-1$
455 return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IOException validating file: " + path, e); //$NON-NLS-1$
456 }
457 try (BufferedRandomAccessFile rafile = new BufferedRandomAccessFile(path, "r")) { //$NON-NLS-1$
458 int lineCount = 0;
459 double matches = 0.0;
460 String line = rafile.getNextLine();
461
462 while ((line != null) && (lineCount++ < MAX_LINES)) {
463 for (InputLine inputLine : fDefinition.inputs) {
464 Matcher matcher = inputLine.getPattern().matcher(line);
465 if (matcher.matches()) {
466 int groupCount = matcher.groupCount();
467 matches += (1.0 + groupCount / ((double) groupCount + 1));
468 break;
469 }
470 }
471 confidence = (int) (MAX_CONFIDENCE * matches / lineCount);
472 line = rafile.getNextLine();
473 }
474 } catch (IOException e) {
475 return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IOException validating file: " + path, e); //$NON-NLS-1$
476 }
477 return new TraceValidationStatus(confidence, Activator.PLUGIN_ID);
478 }
479
480 private static int fCheckpointSize = -1;
481
482 @Override
483 public synchronized int getCheckpointSize() {
484 if (fCheckpointSize == -1) {
485 TmfCheckpoint c = new TmfCheckpoint(TmfTimestamp.ZERO, new TmfLongLocation(0L), 0);
486 ByteBuffer b = ByteBuffer.allocate(ITmfCheckpoint.MAX_SERIALIZE_SIZE);
487 b.clear();
488 c.serialize(b);
489 fCheckpointSize = b.position();
490 }
491
492 return fCheckpointSize;
493 }
494
495 @Override
496 public ITmfLocation restoreLocation(ByteBuffer bufferIn) {
497 return new TmfLongLocation(bufferIn);
498 }
499
500 @Override
501 protected ITmfTraceIndexer createIndexer(int interval) {
502 return new TmfBTreeTraceIndexer(this, interval);
503 }
504
505 @Override
506 public String getTraceTypeId() {
507 return fTraceTypeId;
508 }
509
510 /**
511 * Build the trace type id for a custom text trace
512 *
513 * @param category
514 * the category
515 * @param definitionName
516 * the definition name
517 * @return the trace type id
518 */
519 public static @NonNull String buildTraceTypeId(String category, String definitionName) {
520 return CUSTOM_TXT_TRACE_TYPE_PREFIX + category + SEPARATOR + definitionName;
521 }
522
523 /**
524 * Checks whether the given trace type ID is a custom text trace type ID
525 *
526 * @param traceTypeId
527 * the trace type ID to check
528 * @return <code>true</code> if it's a custom text trace type ID else <code>false</code>
529 */
530 public static boolean isCustomTraceTypeId(@NonNull String traceTypeId) {
531 return traceTypeId.startsWith(CUSTOM_TXT_TRACE_TYPE_PREFIX);
532 }
533
534 /**
535 * This methods builds a trace type ID from a given ID taking into
536 * consideration any format changes that were done for the IDs of custom
537 * text traces. For example, such format change took place when moving to
538 * Trace Compass. Trace type IDs that are part of the plug-in extension for
539 * trace types won't be changed.
540 *
541 * This method is useful for IDs that were persisted in the workspace before
542 * the format changes (e.g. in the persistent properties of a trace
543 * resource).
544 *
545 * It ensures backwards compatibility of the workspace for custom text
546 * traces.
547 *
548 * @param traceTypeId
549 * the legacy trace type ID
550 * @return the trace type id in Trace Compass format
551 */
552 public static @NonNull String buildCompatibilityTraceTypeId(@NonNull String traceTypeId) {
553 // Handle early Trace Compass custom text trace type IDs
554 if (traceTypeId.startsWith(EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX)) {
555 return CUSTOM_TXT_TRACE_TYPE_PREFIX + traceTypeId.substring(EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX.length());
556 }
557
558 // Handle Linux Tools custom text trace type IDs (with and without category)
559 int index = traceTypeId.lastIndexOf(SEPARATOR);
560 if ((index != -1) && (traceTypeId.startsWith(LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX))) {
561 String definitionName = index < traceTypeId.length() ? traceTypeId.substring(index + 1) : ""; //$NON-NLS-1$
562 if (traceTypeId.contains(CustomTxtTrace.class.getSimpleName() + SEPARATOR) && traceTypeId.indexOf(SEPARATOR) == index) {
563 return buildTraceTypeId(CustomTxtTraceDefinition.CUSTOM_TXT_CATEGORY, definitionName);
564 }
565 return CUSTOM_TXT_TRACE_TYPE_PREFIX + traceTypeId.substring(LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX.length());
566 }
567 return traceTypeId;
568 }
569
570 @TmfSignalHandler
571 @Override
572 public void traceRangeUpdated(TmfTraceRangeUpdatedSignal signal) {
573 if (signal.getTrace() == this) {
574 try {
575 synchronized (this) {
576 // Reset the file handle in case it has reached the end of the
577 // file already. Otherwise, it will not be able to read new data
578 // pass the previous end.
579 initFile();
580 }
581 } catch (TmfTraceException e) {
582 Activator.logError(e.getLocalizedMessage(), e);
583 }
584 }
585 super.traceRangeUpdated(signal);
586 }
587
588 /**
589 * @since 3.0
590 */
591 @Override
592 public synchronized ITmfTimestamp readEnd() {
593 try {
594 Long pos = fFile.length() - 1;
595 /* Outer loop to find the first line of a matcher group. */
596 while (pos > 0) {
597 /* Inner loop to find line beginning */
598 while (pos > 0) {
599 fFile.seek(pos - 1);
600 if (fFile.read() == '\n') {
601 break;
602 }
603 pos--;
604 }
605 ITmfLocation location = new TmfLongLocation(pos);
606 ITmfContext context = seekEvent(location);
607 ITmfEvent event = parseEvent(context);
608 context.dispose();
609 if (event != null) {
610 /* The last event in the trace was successfully parsed. */
611 return event.getTimestamp();
612 }
613 /* pos was after the beginning of the lines of the last event. */
614 pos--;
615 }
616 } catch (IOException e) {
617 Activator.logError("Error seeking last event. File: " + getPath(), e); //$NON-NLS-1$
618 }
619
620 /* Empty trace */
621 return null;
622 }
623 }
This page took 0.04925 seconds and 5 git commands to generate.