/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package org.pentaho.di.ui.spoon.trans; import java.awt.BasicStroke; import java.awt.Color; import java.awt.geom.Ellipse2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.LineAndShapeRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.category.DefaultCategoryDataset; import org.pentaho.di.core.Const; import org.pentaho.di.i18n.BaseMessages; import org.pentaho.di.trans.performance.StepPerformanceSnapShot; import org.pentaho.di.ui.core.gui.GUIResource; import org.pentaho.di.ui.spoon.Spoon; import org.pentaho.di.ui.spoon.delegates.SpoonDelegate; import org.pentaho.di.ui.trans.dialog.TransDialog; import org.pentaho.di.ui.util.ImageUtil; public class TransPerfDelegate extends SpoonDelegate { private static Class<?> PKG = Spoon.class; // for i18n purposes, needed by Translator2!! // private static final LogWriter log = LogWriter.getInstance(); private static final int DATA_CHOICE_WRITTEN = 0; private static final int DATA_CHOICE_READ = 1; private static final int DATA_CHOICE_INPUT = 2; private static final int DATA_CHOICE_OUTPUT = 3; private static final int DATA_CHOICE_UPDATED = 4; private static final int DATA_CHOICE_REJECTED = 5; private static final int DATA_CHOICE_INPUT_BUFFER_SIZE = 6; private static final int DATA_CHOICE_OUTPUT_BUFFER_SIZE = 7; private static String[] dataChoices = new String[] { BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Written" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Read" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Input" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Output" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Updated" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Rejected" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.InputBufferSize" ), BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.OutputBufferSize" ), }; private TransGraph transGraph; private CTabItem transPerfTab; private Map<String, List<StepPerformanceSnapShot>> stepPerformanceSnapShots; private String[] steps; private org.eclipse.swt.widgets.List stepsList; private Canvas canvas; private Image image; private long timeDifference; private String title; private org.eclipse.swt.widgets.List dataList; private Composite perfComposite; private boolean emptyGraph; /** * @param spoon * @param transGraph */ public TransPerfDelegate( Spoon spoon, TransGraph transGraph ) { super( spoon ); this.transGraph = transGraph; } public void addTransPerf() { // First, see if we need to add the extra view... // if ( transGraph.extraViewComposite == null || transGraph.extraViewComposite.isDisposed() ) { transGraph.addExtraView(); } else { if ( transPerfTab != null && !transPerfTab.isDisposed() ) { // just set this one active and get out... // transGraph.extraViewTabFolder.setSelection( transPerfTab ); return; } } // Add a transLogTab : display the logging... // transPerfTab = new CTabItem( transGraph.extraViewTabFolder, SWT.NONE ); transPerfTab.setImage( GUIResource.getInstance().getImageShowPerf() ); transPerfTab.setText( BaseMessages.getString( PKG, "Spoon.TransGraph.PerfTab.Name" ) ); // Create a composite, slam everything on there like it was in the history tab. // perfComposite = new Composite( transGraph.extraViewTabFolder, SWT.NONE ); perfComposite.setBackground( GUIResource.getInstance().getColorBackground() ); perfComposite.setLayout( new FormLayout() ); spoon.props.setLook( perfComposite ); transGraph.getDisplay().asyncExec( new Runnable() { public void run() { setupContent(); } } ); transPerfTab.setControl( perfComposite ); transGraph.extraViewTabFolder.setSelection( transPerfTab ); transGraph.extraViewTabFolder.addSelectionListener( new SelectionAdapter() { public void widgetSelected( SelectionEvent arg0 ) { layoutPerfComposite(); updateGraph(); } } ); } public void setupContent() { // there is a potential infinite loop below if this method // is called when the transgraph is not running, so we check // early to make sure it won't happen (see PDI-5009) if ( !transGraph.isRunning() || transGraph.trans == null || !transGraph.trans.getTransMeta().isCapturingStepPerformanceSnapShots() ) { showEmptyGraph(); return; // TODO: display help text and rerty button } if ( perfComposite.isDisposed() ) { return; } // Remove anything on the perf composite, like an empty page message // for ( Control control : perfComposite.getChildren() ) { if ( !control.isDisposed() ) { control.dispose(); } } emptyGraph = false; this.title = transGraph.trans.getTransMeta().getName(); this.timeDifference = transGraph.trans.getTransMeta().getStepPerformanceCapturingDelay(); this.stepPerformanceSnapShots = transGraph.trans.getStepPerformanceSnapShots(); // Wait a second for the first data to pour in... // TODO: make this wait more elegant... // while ( this.stepPerformanceSnapShots == null || stepPerformanceSnapShots.isEmpty() ) { this.stepPerformanceSnapShots = transGraph.trans.getStepPerformanceSnapShots(); try { Thread.sleep( 100L ); } catch ( InterruptedException e ) { // Ignore errors } } Set<String> stepsSet = stepPerformanceSnapShots.keySet(); steps = stepsSet.toArray( new String[stepsSet.size()] ); Arrays.sort( steps ); // Display 2 lists with the data types and the steps on the left side. // Then put a canvas with the graph on the right side // Label dataListLabel = new Label( perfComposite, SWT.NONE ); dataListLabel.setText( BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Metrics.Label" ) ); spoon.props.setLook( dataListLabel ); FormData fdDataListLabel = new FormData(); fdDataListLabel.left = new FormAttachment( 0, 0 ); fdDataListLabel.right = new FormAttachment( spoon.props.getMiddlePct() / 2, Const.MARGIN ); fdDataListLabel.top = new FormAttachment( 0, Const.MARGIN + 5 ); dataListLabel.setLayoutData( fdDataListLabel ); dataList = new org.eclipse.swt.widgets.List( perfComposite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.LEFT | SWT.BORDER ); spoon.props.setLook( dataList ); dataList.setItems( dataChoices ); dataList.addSelectionListener( new SelectionAdapter() { public void widgetSelected( SelectionEvent event ) { // If there are multiple selections here AND there are multiple selections in the steps list, we only take the // first step in the selection... // if ( dataList.getSelectionCount() > 1 && stepsList.getSelectionCount() > 1 ) { stepsList.setSelection( stepsList.getSelectionIndices()[0] ); } updateGraph(); } } ); FormData fdDataList = new FormData(); fdDataList.left = new FormAttachment( 0, 0 ); fdDataList.right = new FormAttachment( spoon.props.getMiddlePct() / 2, Const.MARGIN ); fdDataList.top = new FormAttachment( dataListLabel, Const.MARGIN ); fdDataList.bottom = new FormAttachment( 40, Const.MARGIN ); dataList.setLayoutData( fdDataList ); Label stepsListLabel = new Label( perfComposite, SWT.NONE ); stepsListLabel.setText( BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.Steps.Label" ) ); spoon.props.setLook( stepsListLabel ); FormData fdStepsListLabel = new FormData(); fdStepsListLabel.left = new FormAttachment( 0, 0 ); fdStepsListLabel.right = new FormAttachment( spoon.props.getMiddlePct() / 2, Const.MARGIN ); fdStepsListLabel.top = new FormAttachment( dataList, Const.MARGIN ); stepsListLabel.setLayoutData( fdStepsListLabel ); stepsList = new org.eclipse.swt.widgets.List( perfComposite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.LEFT | SWT.BORDER ); spoon.props.setLook( stepsList ); stepsList.setItems( steps ); stepsList.addSelectionListener( new SelectionAdapter() { public void widgetSelected( SelectionEvent event ) { // If there are multiple selections here AND there are multiple selections in the data list, we only take the // first data item in the selection... // if ( dataList.getSelectionCount() > 1 && stepsList.getSelectionCount() > 1 ) { dataList.setSelection( dataList.getSelectionIndices()[0] ); } updateGraph(); } } ); FormData fdStepsList = new FormData(); fdStepsList.left = new FormAttachment( 0, 0 ); fdStepsList.right = new FormAttachment( spoon.props.getMiddlePct() / 2, Const.MARGIN ); fdStepsList.top = new FormAttachment( stepsListLabel, Const.MARGIN ); fdStepsList.bottom = new FormAttachment( 100, Const.MARGIN ); stepsList.setLayoutData( fdStepsList ); canvas = new Canvas( perfComposite, SWT.NONE ); spoon.props.setLook( canvas ); FormData fdCanvas = new FormData(); fdCanvas.left = new FormAttachment( spoon.props.getMiddlePct() / 2, Const.MARGIN ); fdCanvas.right = new FormAttachment( 100, 0 ); fdCanvas.top = new FormAttachment( 0, Const.MARGIN ); fdCanvas.bottom = new FormAttachment( 100, 0 ); canvas.setLayoutData( fdCanvas ); perfComposite.addControlListener( new ControlAdapter() { public void controlResized( ControlEvent event ) { updateGraph(); } } ); perfComposite.addDisposeListener( new DisposeListener() { public void widgetDisposed( DisposeEvent event ) { if ( image != null ) { image.dispose(); } } } ); canvas.addPaintListener( new PaintListener() { public void paintControl( PaintEvent event ) { if ( image != null && !image.isDisposed() ) { event.gc.drawImage( image, 0, 0 ); } } } ); // Refresh automatically every 5 seconds as well. // final Timer timer = new Timer( "TransPerfDelegate Timer" ); timer.schedule( new TimerTask() { public void run() { updateGraph(); } }, 0, 5000 ); // When the tab is closed, we remove the update timer // transPerfTab.addDisposeListener( new DisposeListener() { public void widgetDisposed( DisposeEvent arg0 ) { timer.cancel(); } } ); } /** * Tell the user that the transformation is not running or that there is no monitoring configured. */ private void showEmptyGraph() { if ( perfComposite.isDisposed() ) { return; } emptyGraph = true; Label label = new Label( perfComposite, SWT.CENTER ); label.setText( BaseMessages.getString( PKG, "TransLog.Dialog.PerformanceMonitoringNotEnabled.Message" ) ); label.setBackground( perfComposite.getBackground() ); label.setFont( GUIResource.getInstance().getFontMedium() ); FormData fdLabel = new FormData(); fdLabel.left = new FormAttachment( 5, 0 ); fdLabel.right = new FormAttachment( 95, 0 ); fdLabel.top = new FormAttachment( 5, 0 ); label.setLayoutData( fdLabel ); Button button = new Button( perfComposite, SWT.CENTER ); button.setText( BaseMessages.getString( PKG, "TransLog.Dialog.PerformanceMonitoring.Button" ) ); button.setBackground( perfComposite.getBackground() ); button.setFont( GUIResource.getInstance().getFontMedium() ); button.addSelectionListener( new SelectionAdapter() { public void widgetSelected( SelectionEvent event ) { TransGraph.editProperties( spoon.getActiveTransformation(), spoon, spoon.rep, true, TransDialog.Tabs.MONITOR_TAB ); } } ); FormData fdButton = new FormData(); fdButton.left = new FormAttachment( 40, 0 ); fdButton.right = new FormAttachment( 60, 0 ); fdButton.top = new FormAttachment( label, 5 ); button.setLayoutData( fdButton ); perfComposite.layout( true, true ); } public void showPerfView() { // What button? // // XulToolbarButton showLogXulButton = toolbar.getButtonById("trans-show-log"); // ToolItem toolBarButton = (ToolItem) showLogXulButton.getNativeObject(); if ( transPerfTab == null || transPerfTab.isDisposed() ) { addTransPerf(); } else { transPerfTab.dispose(); transGraph.checkEmptyExtraView(); } } private void updateGraph() { transGraph.getDisplay().asyncExec( new Runnable() { public void run() { if ( perfComposite != null && !perfComposite.isDisposed() && canvas != null && !canvas.isDisposed() && transPerfTab != null && !transPerfTab.isDisposed() ) { if ( transPerfTab.isShowing() ) { updateCanvas(); } } } } ); } private void updateCanvas() { Rectangle bounds = canvas.getBounds(); if ( bounds.width <= 0 || bounds.height <= 0 ) { return; } // The list of snapshots : convert to JFreeChart dataset // DefaultCategoryDataset dataset = new DefaultCategoryDataset(); String[] selectedSteps = stepsList.getSelection(); if ( selectedSteps == null || selectedSteps.length == 0 ) { selectedSteps = new String[] { steps[0], }; // first step stepsList.select( 0 ); } int[] dataIndices = dataList.getSelectionIndices(); if ( dataIndices == null || dataIndices.length == 0 ) { dataIndices = new int[] { DATA_CHOICE_WRITTEN, }; dataList.select( 0 ); } boolean multiStep = stepsList.getSelectionCount() > 1; boolean multiData = dataList.getSelectionCount() > 1; boolean calcMoving = !multiStep && !multiData; // A single metric shown for a single step List<Double> movingList = new ArrayList<Double>(); int movingSize = 10; double movingTotal = 0; int totalTimeInSeconds = 0; for ( int t = 0; t < selectedSteps.length; t++ ) { String stepNameCopy = selectedSteps[t]; List<StepPerformanceSnapShot> snapShotList = stepPerformanceSnapShots.get( stepNameCopy ); if ( snapShotList != null && snapShotList.size() > 1 ) { totalTimeInSeconds = (int) Math .round( ( (double) ( snapShotList.get( snapShotList.size() - 1 ).getDate().getTime() - snapShotList .get( 0 ).getDate().getTime() ) ) / 1000 ); for ( int i = 0; i < snapShotList.size(); i++ ) { StepPerformanceSnapShot snapShot = snapShotList.get( i ); if ( snapShot.getTimeDifference() != 0 ) { double factor = (double) 1000 / (double) snapShot.getTimeDifference(); for ( int d = 0; d < dataIndices.length; d++ ) { String dataType; if ( multiStep ) { dataType = stepNameCopy; } else { dataType = dataChoices[dataIndices[d]]; } String xLabel = Integer.toString( Math.round( i * timeDifference / 1000 ) ); Double metric = null; switch ( dataIndices[d] ) { case DATA_CHOICE_INPUT: metric = snapShot.getLinesInput() * factor; break; case DATA_CHOICE_OUTPUT: metric = snapShot.getLinesOutput() * factor; break; case DATA_CHOICE_READ: metric = snapShot.getLinesRead() * factor; break; case DATA_CHOICE_WRITTEN: metric = snapShot.getLinesWritten() * factor; break; case DATA_CHOICE_UPDATED: metric = snapShot.getLinesUpdated() * factor; break; case DATA_CHOICE_REJECTED: metric = snapShot.getLinesRejected() * factor; break; case DATA_CHOICE_INPUT_BUFFER_SIZE: metric = (double) snapShot.getInputBufferSize(); break; case DATA_CHOICE_OUTPUT_BUFFER_SIZE: metric = (double) snapShot.getOutputBufferSize(); break; default: break; } if ( metric != null ) { dataset.addValue( metric, dataType, xLabel ); if ( calcMoving ) { movingTotal += metric; movingList.add( metric ); if ( movingList.size() > movingSize ) { movingTotal -= movingList.get( 0 ); movingList.remove( 0 ); } double movingAverage = movingTotal / movingList.size(); dataset.addValue( movingAverage, dataType + "(Avg)", xLabel ); // System.out.println("moving average = "+movingAverage+", movingTotal="+movingTotal+", m"); } } } } } } } String chartTitle = title; if ( multiStep ) { chartTitle += " (" + dataChoices[dataIndices[0]] + ")"; } else { chartTitle += " (" + selectedSteps[0] + ")"; } final JFreeChart chart = ChartFactory.createLineChart( chartTitle, // chart title BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.TimeInSeconds.Label", Integer .toString( totalTimeInSeconds ), Long.toString( timeDifference ) ), // domain axis label BaseMessages.getString( PKG, "StepPerformanceSnapShotDialog.RowsPerSecond.Label" ), // range axis label dataset, // data PlotOrientation.VERTICAL, // orientation true, // include legend true, // tooltips false ); // urls chart.setBackgroundPaint( Color.white ); TextTitle title = new TextTitle( chartTitle ); // title.setExpandToFitSpace(true); // org.eclipse.swt.graphics.Color pentahoColor = GUIResource.getInstance().getColorPentaho(); // java.awt.Color color = new java.awt.Color(pentahoColor.getRed(), pentahoColor.getGreen(),pentahoColor.getBlue()); // title.setBackgroundPaint(color); title.setFont( new java.awt.Font( "SansSerif", java.awt.Font.BOLD, 12 ) ); chart.setTitle( title ); CategoryPlot plot = (CategoryPlot) chart.getPlot(); plot.setBackgroundPaint( Color.white ); plot.setForegroundAlpha( 0.5f ); plot.setRangeGridlinesVisible( true ); NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); rangeAxis.setStandardTickUnits( NumberAxis.createIntegerTickUnits() ); CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setTickLabelsVisible( false ); // Customize the renderer... // LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer(); renderer.setBaseShapesVisible( true ); renderer.setDrawOutlines( true ); renderer.setUseFillPaint( true ); renderer.setBaseFillPaint( Color.white ); renderer.setSeriesStroke( 0, new BasicStroke( 1.5f ) ); renderer.setSeriesOutlineStroke( 0, new BasicStroke( 1.5f ) ); renderer.setSeriesStroke( 1, new BasicStroke( 2.5f ) ); renderer.setSeriesOutlineStroke( 1, new BasicStroke( 2.5f ) ); renderer.setSeriesShape( 0, new Ellipse2D.Double( -3.0, -3.0, 6.0, 6.0 ) ); BufferedImage bufferedImage = chart.createBufferedImage( bounds.width, bounds.height ); ImageData imageData = ImageUtil.convertToSWT( bufferedImage ); // dispose previous image... // if ( image != null ) { image.dispose(); } image = new Image( transGraph.getDisplay(), imageData ); // Draw the image on the canvas... // canvas.redraw(); } /** * @return the transHistoryTab */ public CTabItem getTransPerfTab() { return transPerfTab; } /** * @return the emptyGraph */ public boolean isEmptyGraph() { return emptyGraph; } public void layoutPerfComposite() { if ( !perfComposite.isDisposed() ) { perfComposite.layout( true, true ); } } }