Fix for bug 381096 (fix for ClassCastException)
[deliverable/tracecompass.git] / org.eclipse.linuxtools.lttng2.kernel.ui / src / org / eclipse / linuxtools / internal / lttng2 / kernel / ui / views / resources / ResourcesView.java
1 /*******************************************************************************
2 * Copyright (c) 2012 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 *******************************************************************************/
12
13 package org.eclipse.linuxtools.internal.lttng2.kernel.ui.views.resources;
14
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.Comparator;
18 import java.util.Iterator;
19 import java.util.List;
20
21 import org.eclipse.core.runtime.IProgressMonitor;
22 import org.eclipse.core.runtime.NullProgressMonitor;
23 import org.eclipse.jface.action.Action;
24 import org.eclipse.jface.action.IToolBarManager;
25 import org.eclipse.jface.action.Separator;
26 import org.eclipse.linuxtools.internal.lttng2.kernel.core.Attributes;
27 import org.eclipse.linuxtools.internal.lttng2.kernel.ui.Messages;
28 import org.eclipse.linuxtools.internal.lttng2.kernel.ui.views.resources.ResourcesEntry.Type;
29 import org.eclipse.linuxtools.lttng2.kernel.core.trace.CtfKernelTrace;
30 import org.eclipse.linuxtools.tmf.core.ctfadaptor.CtfTmfTimestamp;
31 import org.eclipse.linuxtools.tmf.core.event.ITmfEvent;
32 import org.eclipse.linuxtools.tmf.core.event.TmfEvent;
33 import org.eclipse.linuxtools.tmf.core.event.TmfTimeRange;
34 import org.eclipse.linuxtools.tmf.core.event.TmfTimestamp;
35 import org.eclipse.linuxtools.tmf.core.exceptions.AttributeNotFoundException;
36 import org.eclipse.linuxtools.tmf.core.exceptions.StateValueTypeException;
37 import org.eclipse.linuxtools.tmf.core.exceptions.TimeRangeException;
38 import org.eclipse.linuxtools.tmf.core.interval.ITmfStateInterval;
39 import org.eclipse.linuxtools.tmf.core.signal.TmfExperimentSelectedSignal;
40 import org.eclipse.linuxtools.tmf.core.signal.TmfRangeSynchSignal;
41 import org.eclipse.linuxtools.tmf.core.signal.TmfSignalHandler;
42 import org.eclipse.linuxtools.tmf.core.signal.TmfTimeSynchSignal;
43 import org.eclipse.linuxtools.tmf.core.statesystem.IStateSystemQuerier;
44 import org.eclipse.linuxtools.tmf.core.trace.ITmfTrace;
45 import org.eclipse.linuxtools.tmf.core.trace.TmfExperiment;
46 import org.eclipse.linuxtools.tmf.ui.views.TmfView;
47 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.ITimeGraphRangeListener;
48 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.ITimeGraphTimeListener;
49 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.TimeGraphRangeUpdateEvent;
50 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.TimeGraphTimeEvent;
51 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.TimeGraphViewer;
52 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.model.ITimeEvent;
53 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
54 import org.eclipse.linuxtools.tmf.ui.widgets.timegraph.model.TimeEvent;
55 import org.eclipse.swt.SWT;
56 import org.eclipse.swt.widgets.Composite;
57 import org.eclipse.swt.widgets.Display;
58 import org.eclipse.ui.IActionBars;
59
60 public class ResourcesView extends TmfView {
61
62 // ------------------------------------------------------------------------
63 // Constants
64 // ------------------------------------------------------------------------
65
66 /**
67 * View ID.
68 */
69 public static final String ID = "org.eclipse.linuxtools.lttng2.kernel.ui.views.resources"; //$NON-NLS-1$
70
71 /**
72 * Initial time range
73 */
74 private static final long INITIAL_WINDOW_OFFSET = (1L * 100 * 1000 * 1000); // .1sec
75
76 // ------------------------------------------------------------------------
77 // Fields
78 // ------------------------------------------------------------------------
79
80 // The time graph viewer
81 TimeGraphViewer fTimeGraphViewer;
82
83 // The selected experiment
84 private TmfExperiment<ITmfEvent> fSelectedExperiment;
85
86 // The time graph entry list
87 private ArrayList<TraceEntry> fEntryList;
88
89 // The start time
90 private long fStartTime;
91
92 // The end time
93 private long fEndTime;
94
95 // The display width
96 private int fDisplayWidth;
97
98 // The next resource action
99 private Action fNextResourceAction;
100
101 // The previous resource action
102 private Action fPreviousResourceAction;
103
104 // The zoom thread
105 private ZoomThread fZoomThread;
106
107 // ------------------------------------------------------------------------
108 // Classes
109 // ------------------------------------------------------------------------
110
111 private class TraceEntry implements ITimeGraphEntry {
112 // The Trace
113 private CtfKernelTrace fTrace;
114 // The start time
115 private long fTraceStartTime;
116 // The end time
117 private long fTraceEndTime;
118 // The children of the entry
119 private ArrayList<ResourcesEntry> fChildren;
120 // The name of entry
121 private String fName;
122
123 public TraceEntry(CtfKernelTrace trace, String name, long startTime, long endTime) {
124 fTrace = trace;
125 fChildren = new ArrayList<ResourcesEntry>();
126 fName = name;
127 fTraceStartTime = startTime;
128 fTraceEndTime = endTime;
129 }
130
131 @Override
132 public ITimeGraphEntry getParent() {
133 return null;
134 }
135
136 @Override
137 public boolean hasChildren() {
138 return fChildren != null && fChildren.size() > 0;
139 }
140
141 @Override
142 public ResourcesEntry[] getChildren() {
143 return fChildren.toArray(new ResourcesEntry[0]);
144 }
145
146 @Override
147 public String getName() {
148 return fName;
149 }
150
151 @Override
152 public long getStartTime() {
153 return fTraceStartTime;
154 }
155
156 @Override
157 public long getEndTime() {
158 return fTraceEndTime;
159 }
160
161 @Override
162 public boolean hasTimeEvents() {
163 return false;
164 }
165
166 @Override
167 public Iterator<ITimeEvent> getTimeEventsIterator() {
168 return null;
169 }
170
171 @Override
172 public <T extends ITimeEvent> Iterator<T> getTimeEventsIterator(long startTime, long stopTime, long visibleDuration) {
173 return null;
174 }
175
176 public CtfKernelTrace getTrace() {
177 return fTrace;
178 }
179
180 public void addChild(ResourcesEntry entry) {
181 int index;
182 for (index = 0; index < fChildren.size(); index++) {
183 ResourcesEntry other = fChildren.get(index);
184 if (entry.getType().compareTo(other.getType()) < 0) {
185 break;
186 } else if (entry.getType().equals(other.getType())) {
187 if (entry.getId() < other.getId()) {
188 break;
189 }
190 }
191 }
192 entry.setParent(this);
193 fChildren.add(index, entry);
194 }
195 }
196
197 private static class TraceEntryComparator implements Comparator<ITimeGraphEntry> {
198 @Override
199 public int compare(ITimeGraphEntry o1, ITimeGraphEntry o2) {
200 int result = o1.getStartTime() < o2.getStartTime() ? -1 : o1.getStartTime() > o2.getStartTime() ? 1 : 0;
201 if (result == 0) {
202 result = o1.getName().compareTo(o2.getName());
203 }
204 return result;
205 }
206 }
207
208 private class ZoomThread extends Thread {
209 private long fZoomStartTime;
210 private long fZoomEndTime;
211 private IProgressMonitor fMonitor;
212
213 public ZoomThread(long startTime, long endTime) {
214 super("ResourcesView zoom"); //$NON-NLS-1$
215 fZoomStartTime = startTime;
216 fZoomEndTime = endTime;
217 fMonitor = new NullProgressMonitor();
218 }
219
220 @Override
221 public void run() {
222 ArrayList<TraceEntry> entryList = fEntryList;
223 if (entryList == null) {
224 return;
225 }
226 long resolution = Math.max(1, (fZoomEndTime - fZoomStartTime) / fDisplayWidth);
227 for (TraceEntry traceEntry : entryList) {
228 for (ITimeGraphEntry child : traceEntry.getChildren()) {
229 ResourcesEntry entry = (ResourcesEntry) child;
230 if (fMonitor.isCanceled()) {
231 break;
232 }
233 List<ITimeEvent> zoomedEventList = getEventList(entry, fZoomStartTime, fZoomEndTime, resolution, true, fMonitor);
234 if (fMonitor.isCanceled()) {
235 break;
236 }
237 entry.setZoomedEventList(zoomedEventList);
238 redraw();
239 }
240 }
241 }
242
243 public void cancel() {
244 fMonitor.setCanceled(true);
245 }
246 }
247
248 // ------------------------------------------------------------------------
249 // Constructors
250 // ------------------------------------------------------------------------
251
252 public ResourcesView() {
253 super(ID);
254 fDisplayWidth = Display.getDefault().getBounds().width;
255 }
256
257 // ------------------------------------------------------------------------
258 // ViewPart
259 // ------------------------------------------------------------------------
260
261 /* (non-Javadoc)
262 * @see org.eclipse.linuxtools.tmf.ui.views.TmfView#createPartControl(org.eclipse.swt.widgets.Composite)
263 */
264 @Override
265 public void createPartControl(Composite parent) {
266 fTimeGraphViewer = new TimeGraphViewer(parent, SWT.NONE);
267
268 fTimeGraphViewer.setTimeGraphProvider(new ResourcesPresentationProvider());
269
270 fTimeGraphViewer.setTimeCalendarFormat(true);
271
272 fTimeGraphViewer.addRangeListener(new ITimeGraphRangeListener() {
273 @Override
274 public void timeRangeUpdated(TimeGraphRangeUpdateEvent event) {
275 long startTime = event.getStartTime();
276 long endTime = event.getEndTime();
277 TmfTimeRange range = new TmfTimeRange(new CtfTmfTimestamp(startTime), new CtfTmfTimestamp(endTime));
278 TmfTimestamp time = new CtfTmfTimestamp(fTimeGraphViewer.getSelectedTime());
279 broadcast(new TmfRangeSynchSignal(ResourcesView.this, range, time));
280 startZoomThread(startTime, endTime);
281 }
282 });
283
284 fTimeGraphViewer.addTimeListener(new ITimeGraphTimeListener() {
285 @Override
286 public void timeSelected(TimeGraphTimeEvent event) {
287 long time = event.getTime();
288 broadcast(new TmfTimeSynchSignal(ResourcesView.this, new CtfTmfTimestamp(time)));
289 }
290 });
291
292 final Thread thread = new Thread("ResourcesView build") { //$NON-NLS-1$
293 @Override
294 public void run() {
295 if (TmfExperiment.getCurrentExperiment() != null) {
296 selectExperiment(TmfExperiment.getCurrentExperiment());
297 }
298 }
299 };
300 thread.start();
301
302 // View Action Handling
303 makeActions();
304 contributeToActionBars();
305 }
306
307 /* (non-Javadoc)
308 * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
309 */
310 @Override
311 public void setFocus() {
312 fTimeGraphViewer.setFocus();
313 }
314
315 // ------------------------------------------------------------------------
316 // Signal handlers
317 // ------------------------------------------------------------------------
318
319 @TmfSignalHandler
320 public void experimentSelected(final TmfExperimentSelectedSignal<? extends TmfEvent> signal) {
321 if (signal.getExperiment().equals(fSelectedExperiment)) {
322 return;
323 }
324
325 final Thread thread = new Thread("ResourcesView build") { //$NON-NLS-1$
326 @Override
327 public void run() {
328 selectExperiment(signal.getExperiment());
329 }
330 };
331 thread.start();
332 }
333
334 @TmfSignalHandler
335 public void synchToTime(final TmfTimeSynchSignal signal) {
336 if (signal.getSource() == this) {
337 return;
338 }
339 final long time = signal.getCurrentTime().normalize(0, -9).getValue();
340 Display.getDefault().asyncExec(new Runnable() {
341 @Override
342 public void run() {
343 if (fTimeGraphViewer.getControl().isDisposed()) {
344 return;
345 }
346 fTimeGraphViewer.setSelectedTime(time, true);
347 }
348 });
349 }
350
351 @TmfSignalHandler
352 public void synchToRange(final TmfRangeSynchSignal signal) {
353 if (signal.getSource() == this) {
354 return;
355 }
356 final long startTime = signal.getCurrentRange().getStartTime().normalize(0, -9).getValue();
357 final long endTime = signal.getCurrentRange().getEndTime().normalize(0, -9).getValue();
358 final long time = signal.getCurrentTime().normalize(0, -9).getValue();
359 Display.getDefault().asyncExec(new Runnable() {
360 @Override
361 public void run() {
362 if (fTimeGraphViewer.getControl().isDisposed()) {
363 return;
364 }
365 fTimeGraphViewer.setStartFinishTime(startTime, endTime);
366 fTimeGraphViewer.setSelectedTime(time, false);
367 startZoomThread(startTime, endTime);
368 }
369 });
370 }
371
372 // ------------------------------------------------------------------------
373 // Internal
374 // ------------------------------------------------------------------------
375
376 @SuppressWarnings("unchecked")
377 private void selectExperiment(TmfExperiment<?> experiment) {
378 fStartTime = Long.MAX_VALUE;
379 fEndTime = Long.MIN_VALUE;
380 fSelectedExperiment = (TmfExperiment<ITmfEvent>) experiment;
381 ArrayList<TraceEntry> entryList = new ArrayList<TraceEntry>();
382 for (ITmfTrace<?> trace : experiment.getTraces()) {
383 if (trace instanceof CtfKernelTrace) {
384 CtfKernelTrace ctfKernelTrace = (CtfKernelTrace) trace;
385 IStateSystemQuerier ssq = ctfKernelTrace.getStateSystem();
386 long startTime = ssq.getStartTime();
387 long endTime = ssq.getCurrentEndTime() + 1;
388 TraceEntry groupEntry = new TraceEntry(ctfKernelTrace, trace.getName(), startTime, endTime);
389 entryList.add(groupEntry);
390 fStartTime = Math.min(fStartTime, startTime);
391 fEndTime = Math.max(fEndTime, endTime);
392 List<Integer> cpuQuarks = ssq.getQuarks(Attributes.CPUS, "*"); //$NON-NLS-1$
393 ResourcesEntry[] cpuEntries = new ResourcesEntry[cpuQuarks.size()];
394 for (int i = 0; i < cpuQuarks.size(); i++) {
395 int cpuQuark = cpuQuarks.get(i);
396 int cpu = Integer.parseInt(ssq.getAttributeName(cpuQuark));
397 ResourcesEntry entry = new ResourcesEntry(cpuQuark, ctfKernelTrace, Type.CPU, cpu);
398 groupEntry.addChild(entry);
399 cpuEntries[i] = entry;
400 }
401 List<Integer> irqQuarks = ssq.getQuarks(Attributes.RESOURCES, Attributes.IRQS, "*"); //$NON-NLS-1$
402 ResourcesEntry[] irqEntries = new ResourcesEntry[irqQuarks.size()];
403 for (int i = 0; i < irqQuarks.size(); i++) {
404 int irqQuark = irqQuarks.get(i);
405 int irq = Integer.parseInt(ssq.getAttributeName(irqQuark));
406 ResourcesEntry entry = new ResourcesEntry(irqQuark, ctfKernelTrace, Type.IRQ, irq);
407 groupEntry.addChild(entry);
408 irqEntries[i] = entry;
409 }
410 List<Integer> softIrqQuarks = ssq.getQuarks(Attributes.RESOURCES, Attributes.SOFT_IRQS, "*"); //$NON-NLS-1$
411 ResourcesEntry[] softIrqEntries = new ResourcesEntry[softIrqQuarks.size()];
412 for (int i = 0; i < softIrqQuarks.size(); i++) {
413 int softIrqQuark = softIrqQuarks.get(i);
414 int softIrq = Integer.parseInt(ssq.getAttributeName(softIrqQuark));
415 ResourcesEntry entry = new ResourcesEntry(softIrqQuark, ctfKernelTrace, Type.SOFT_IRQ, softIrq);
416 groupEntry.addChild(entry);
417 softIrqEntries[i] = entry;
418 }
419 }
420 }
421 fEntryList = entryList;
422 refresh(INITIAL_WINDOW_OFFSET);
423 for (TraceEntry traceEntry : fEntryList) {
424 CtfKernelTrace ctfKernelTrace = ((TraceEntry) traceEntry).getTrace();
425 IStateSystemQuerier ssq = ctfKernelTrace.getStateSystem();
426 long startTime = ssq.getStartTime();
427 long endTime = ssq.getCurrentEndTime() + 1;
428 long resolution = (endTime - startTime) / fDisplayWidth;
429 for (ResourcesEntry entry : traceEntry.getChildren()) {
430 List<ITimeEvent> eventList = getEventList(entry, startTime, endTime, resolution, false, new NullProgressMonitor());
431 entry.setEventList(eventList);
432 redraw();
433 }
434 }
435 }
436
437 private List<ITimeEvent> getEventList(ResourcesEntry entry, long startTime, long endTime, long resolution, boolean includeNull, IProgressMonitor monitor) {
438 if (endTime <= startTime) {
439 return null;
440 }
441 IStateSystemQuerier ssq = entry.getTrace().getStateSystem();
442 List<ITimeEvent> eventList = null;
443 int quark = entry.getQuark();
444 try {
445 if (entry.getType().equals(Type.CPU)) {
446 int statusQuark = ssq.getQuarkRelative(quark, Attributes.STATUS);
447 List<ITmfStateInterval> statusIntervals = ssq.queryHistoryRange(statusQuark, startTime, endTime - 1, resolution);
448 eventList = new ArrayList<ITimeEvent>(statusIntervals.size());
449 long lastEndTime = -1;
450 for (ITmfStateInterval statusInterval : statusIntervals) {
451 if (monitor.isCanceled()) {
452 return null;
453 }
454 int status = statusInterval.getStateValue().unboxInt();
455 long time = statusInterval.getStartTime();
456 long duration = statusInterval.getEndTime() - time + 1;
457 if (!statusInterval.getStateValue().isNull()) {
458 if (lastEndTime != time && lastEndTime != -1) {
459 eventList.add(new TimeEvent(entry, lastEndTime, time - lastEndTime));
460 }
461 eventList.add(new ResourcesEvent(entry, time, duration, status));
462 lastEndTime = time + duration;
463 } else {
464 if (includeNull) {
465 eventList.add(new ResourcesEvent(entry, time, duration));
466 }
467 }
468 }
469 } else if (entry.getType().equals(Type.IRQ)) {
470 List<ITmfStateInterval> irqIntervals = ssq.queryHistoryRange(quark, startTime, endTime - 1, resolution);
471 eventList = new ArrayList<ITimeEvent>(irqIntervals.size());
472 long lastEndTime = -1;
473 boolean lastIsNull = true;
474 for (ITmfStateInterval irqInterval : irqIntervals) {
475 if (monitor.isCanceled()) {
476 return null;
477 }
478 long time = irqInterval.getStartTime();
479 long duration = irqInterval.getEndTime() - time + 1;
480 if (!irqInterval.getStateValue().isNull()) {
481 int cpu = irqInterval.getStateValue().unboxInt();
482 eventList.add(new ResourcesEvent(entry, time, duration, cpu));
483 lastIsNull = false;
484 } else {
485 if (lastEndTime != time && lastEndTime != -1 && lastIsNull) {
486 eventList.add(new ResourcesEvent(entry, lastEndTime, time - lastEndTime, -1));
487 }
488 if (includeNull) {
489 eventList.add(new ResourcesEvent(entry, time, duration));
490 }
491 lastIsNull = true;
492 }
493 lastEndTime = time + duration;
494 }
495 } else if (entry.getType().equals(Type.SOFT_IRQ)) {
496 List<ITmfStateInterval> softIrqIntervals = ssq.queryHistoryRange(quark, startTime, endTime - 1, resolution);
497 eventList = new ArrayList<ITimeEvent>(softIrqIntervals.size());
498 long lastEndTime = -1;
499 boolean lastIsNull = true;
500 for (ITmfStateInterval softIrqInterval : softIrqIntervals) {
501 if (monitor.isCanceled()) {
502 return null;
503 }
504 long time = softIrqInterval.getStartTime();
505 long duration = softIrqInterval.getEndTime() - time + 1;
506 if (!softIrqInterval.getStateValue().isNull()) {
507 int cpu = softIrqInterval.getStateValue().unboxInt();
508 eventList.add(new ResourcesEvent(entry, time, duration, cpu));
509 } else {
510 if (lastEndTime != time && lastEndTime != -1 && lastIsNull) {
511 eventList.add(new ResourcesEvent(entry, lastEndTime, time - lastEndTime, -1));
512 }
513 if (includeNull) {
514 eventList.add(new ResourcesEvent(entry, time, duration));
515 }
516 lastIsNull = true;
517 }
518 lastEndTime = time + duration;
519 }
520 }
521 } catch (AttributeNotFoundException e) {
522 e.printStackTrace();
523 } catch (TimeRangeException e) {
524 e.printStackTrace();
525 } catch (StateValueTypeException e) {
526 e.printStackTrace();
527 }
528 return eventList;
529 }
530
531 private void refresh(final long windowRange) {
532 Display.getDefault().asyncExec(new Runnable() {
533 @Override
534 public void run() {
535 if (fTimeGraphViewer.getControl().isDisposed()) {
536 return;
537 }
538 ITimeGraphEntry[] entries = fEntryList.toArray(new ITimeGraphEntry[0]);
539 Arrays.sort(entries, new TraceEntryComparator());
540 fTimeGraphViewer.setInput(entries);
541 fTimeGraphViewer.setTimeBounds(fStartTime, fEndTime);
542
543 long endTime = fStartTime + windowRange;
544
545 if (fEndTime < endTime) {
546 endTime = fEndTime;
547 }
548 fTimeGraphViewer.setStartFinishTime(fStartTime, endTime);
549
550 startZoomThread(fStartTime, endTime);
551 }
552 });
553 }
554
555
556 private void redraw() {
557 Display.getDefault().asyncExec(new Runnable() {
558 @Override
559 public void run() {
560 if (fTimeGraphViewer.getControl().isDisposed()) {
561 return;
562 }
563 fTimeGraphViewer.getControl().redraw();
564 fTimeGraphViewer.getControl().update();
565 }
566 });
567 }
568
569 private void startZoomThread(long startTime, long endTime) {
570 if (fZoomThread != null) {
571 fZoomThread.cancel();
572 }
573 fZoomThread = new ZoomThread(startTime, endTime);
574 fZoomThread.start();
575 }
576
577 private void makeActions() {
578 fPreviousResourceAction = fTimeGraphViewer.getPreviousItemAction();
579 fPreviousResourceAction.setText(Messages.ResourcesView_previousResourceActionNameText);
580 fPreviousResourceAction.setToolTipText(Messages.ResourcesView_previousResourceActionToolTipText);
581 fNextResourceAction = fTimeGraphViewer.getNextItemAction();
582 fNextResourceAction.setText(Messages.ResourcesView_nextResourceActionNameText);
583 fNextResourceAction.setToolTipText(Messages.ResourcesView_previousResourceActionToolTipText);
584 }
585
586 private void contributeToActionBars() {
587 IActionBars bars = getViewSite().getActionBars();
588 fillLocalToolBar(bars.getToolBarManager());
589 }
590
591 private void fillLocalToolBar(IToolBarManager manager) {
592 manager.add(fTimeGraphViewer.getShowLegendAction());
593 manager.add(new Separator());
594 manager.add(fTimeGraphViewer.getResetScaleAction());
595 manager.add(fTimeGraphViewer.getPreviousEventAction());
596 manager.add(fTimeGraphViewer.getNextEventAction());
597 manager.add(fPreviousResourceAction);
598 manager.add(fNextResourceAction);
599 manager.add(fTimeGraphViewer.getZoomInAction());
600 manager.add(fTimeGraphViewer.getZoomOutAction());
601 manager.add(new Separator());
602 }
603 }
This page took 0.073042 seconds and 6 git commands to generate.