1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * Copyright (c) 2019, Red Hat Inc. All rights reserved.
   4  *
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * The contents of this file are subject to the terms of either the Universal Permissive License
   8  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   9  *
  10  * or the following license:
  11  *
  12  * Redistribution and use in source and binary forms, with or without modification, are permitted
  13  * provided that the following conditions are met:
  14  *
  15  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  16  * and the following disclaimer.
  17  *
  18  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  19  * conditions and the following disclaimer in the documentation and/or other materials provided with
  20  * the distribution.
  21  *
  22  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  23  * endorse or promote products derived from this software without specific prior written permission.
  24  *
  25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  26  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  27  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  31  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  32  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33  */
  34 package org.openjdk.jmc.flightrecorder.ui.pages;
  35 
  36 import static org.openjdk.jmc.common.item.Aggregators.max;
  37 import static org.openjdk.jmc.common.item.Aggregators.min;
  38 
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.List;
  42 import java.util.stream.Collectors;
  43 import java.util.stream.Stream;
  44 
  45 import org.eclipse.jface.action.IAction;
  46 import org.eclipse.jface.action.Separator;
  47 import org.eclipse.jface.resource.ImageDescriptor;
  48 import org.eclipse.jface.viewers.StructuredSelection;
  49 import org.eclipse.jface.viewers.TableViewer;
  50 import org.eclipse.jface.wizard.WizardPage;
  51 import org.eclipse.osgi.util.NLS;
  52 import org.eclipse.swt.SWT;
  53 import org.eclipse.swt.layout.GridData;
  54 import org.eclipse.swt.widgets.Composite;
  55 import org.eclipse.swt.widgets.Item;
  56 import org.eclipse.ui.forms.widgets.FormToolkit;
  57 import org.openjdk.jmc.common.IMCThread;
  58 import org.openjdk.jmc.common.IState;
  59 import org.openjdk.jmc.common.IWritableState;
  60 import org.openjdk.jmc.common.item.Aggregators;
  61 import org.openjdk.jmc.common.item.IAggregator;
  62 import org.openjdk.jmc.common.item.IItemCollection;
  63 import org.openjdk.jmc.common.item.IItemFilter;
  64 import org.openjdk.jmc.common.item.ItemFilters;
  65 import org.openjdk.jmc.common.unit.IQuantity;
  66 import org.openjdk.jmc.common.unit.IRange;
  67 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  68 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
  69 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
  70 import org.openjdk.jmc.flightrecorder.rules.util.JfrRuleTopics;
  71 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
  72 import org.openjdk.jmc.flightrecorder.ui.IDataPageFactory;
  73 import org.openjdk.jmc.flightrecorder.ui.IDisplayablePage;
  74 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
  75 import org.openjdk.jmc.flightrecorder.ui.IPageDefinition;
  76 import org.openjdk.jmc.flightrecorder.ui.IPageUI;
  77 import org.openjdk.jmc.flightrecorder.ui.StreamModel;
  78 import org.openjdk.jmc.flightrecorder.ui.common.AbstractDataPage;
  79 import org.openjdk.jmc.flightrecorder.ui.common.FilterComponent;
  80 import org.openjdk.jmc.flightrecorder.ui.common.FlavorSelector.FlavorSelectorState;
  81 import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants;
  82 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram;
  83 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.HistogramSelection;
  84 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.ItemHistogramBuilder;
  85 import org.openjdk.jmc.flightrecorder.ui.common.ItemRow;
  86 import org.openjdk.jmc.flightrecorder.ui.common.DropdownLaneFilter;
  87 import org.openjdk.jmc.flightrecorder.ui.common.ThreadGraphLanes;
  88 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  89 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStoreActionToolkit;
  90 import org.openjdk.jmc.ui.UIPlugin;
  91 import org.openjdk.jmc.ui.charts.IXDataRenderer;
  92 import org.openjdk.jmc.ui.charts.QuantitySpanRenderer;
  93 import org.openjdk.jmc.ui.charts.RendererToolkit;
  94 import org.openjdk.jmc.ui.column.ColumnManager.SelectionState;
  95 import org.openjdk.jmc.ui.column.ColumnMenusFactory;
  96 import org.openjdk.jmc.ui.column.TableSettings;
  97 import org.openjdk.jmc.ui.handlers.ActionToolkit;
  98 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  99 import org.openjdk.jmc.ui.wizards.IPerformFinishable;
 100 import org.openjdk.jmc.ui.wizards.OnePageWizardDialog;
 101 
 102 public class ThreadsPage extends AbstractDataPage {
 103 
 104         public static class ThreadsPageFactory implements IDataPageFactory {
 105 
 106                 @Override
 107                 public String getName(IState state) {
 108                         return Messages.ThreadsPage_NAME;
 109                 }
 110 
 111                 @Override
 112                 public String[] getTopics(IState state) {
 113                         return new String[] {JfrRuleTopics.THREADS_TOPIC};
 114                 }
 115 
 116                 @Override
 117                 public ImageDescriptor getImageDescriptor(IState state) {
 118                         return FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.PAGE_THREADS);
 119                 }
 120 
 121                 @Override
 122                 public IDisplayablePage createPage(IPageDefinition definition, StreamModel items, IPageContainer editor) {
 123                         return new ThreadsPage(definition, items, editor);
 124                 }
 125 
 126         }
 127 
 128         private static final String THREAD_START_COL = "threadStart"; //$NON-NLS-1$
 129         private static final String THREAD_END_COL = "threadEnd"; //$NON-NLS-1$
 130         private static final String THREAD_DURATION_COL = "threadDuration"; //$NON-NLS-1$
 131         private static final String THREAD_LANE = "threadLane"; //$NON-NLS-1$
 132 
 133         private static final IItemFilter pageFilter = ItemFilters.hasAttribute(JfrAttributes.EVENT_THREAD);
 134         private static final ItemHistogramBuilder HISTOGRAM = new ItemHistogramBuilder();
 135 
 136         static {
 137                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_GROUP_NAME);
 138                 HISTOGRAM.addColumn(JdkAttributes.EVENT_THREAD_ID);
 139                 HISTOGRAM.addColumn(THREAD_START_COL,
 140                                 min(Messages.JavaApplicationPage_COLUMN_THREAD_START,
 141                                                 Messages.JavaApplicationPage_COLUMN_THREAD_START_DESC, JdkTypeIDs.JAVA_THREAD_START,
 142                                                 JfrAttributes.EVENT_TIMESTAMP));
 143                 /*
 144                  * Will order empty cells before first end time.
 145                  * 
 146                  * It should be noted that no event (empty column cell) is considered less than all values
 147                  * (this is common for all columns), which causes the column to sort threads without end
 148                  * time (indicating that the thread ended after the end of the recording) is ordered before
 149                  * the thread that ended first. While this is not optimal, we decided to accept it as it's
 150                  * not obviously better to have this particular column ordering empty cells last in contrast
 151                  * to all other columns.
 152                  */
 153                 HISTOGRAM.addColumn(THREAD_END_COL,
 154                                 max(Messages.JavaApplicationPage_COLUMN_THREAD_END, Messages.JavaApplicationPage_COLUMN_THREAD_END_DESC,
 155                                                 JdkTypeIDs.JAVA_THREAD_END, JfrAttributes.EVENT_TIMESTAMP));
 156                 HISTOGRAM.addColumn(THREAD_DURATION_COL, ic -> {
 157                         IQuantity threadStart = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_START))
 158                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.min(JfrAttributes.EVENT_TIMESTAMP));
 159                         IQuantity threadEnd = ic.apply(ItemFilters.type(JdkTypeIDs.JAVA_THREAD_END))
 160                                         .getAggregate((IAggregator<IQuantity, ?>) Aggregators.max(JfrAttributes.EVENT_TIMESTAMP));
 161                         if (threadStart != null && threadEnd != null) {
 162                                 return threadEnd.subtract(threadStart);
 163                         }
 164                         return null;
 165                 }, Messages.JavaApplicationPage_COLUMN_THREAD_DURATION,
 166                                 Messages.JavaApplicationPage_COLUMN_THREAD_DURATION_DESC);
 167         }
 168 
 169         private class ThreadsPageUi extends ChartAndPopupTableUI {
 170                 private static final String THREADS_TABLE_FILTER = "threadsTableFilter"; //$NON-NLS-1$
 171                 private static final String HIDE_THREAD = "hideThread"; //$NON-NLS-1$
 172                 private static final String RESET_CHART = "resetChart"; //$NON-NLS-1$
 173                 private static final String TABLE = "table"; //$NON-NLS-1$
 174                 private Boolean isChartMenuActionsInit;
 175                 private Boolean isChartModified;
 176                 private Boolean reloadThreads;
 177                 private IAction hideThreadAction;
 178                 private IAction resetChartAction;
 179                 private List<IXDataRenderer> threadRows;
 180                 private MCContextMenuManager mm;
 181                 private ThreadGraphLanes lanes;
 182                 private DropdownLaneFilter laneFilter;
 183 
 184                 ThreadsPageUi(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 185                         super(pageFilter, getDataSource(), parent, toolkit, editor, state, getName(), pageFilter, getIcon(),
 186                                         flavorSelectorState);
 187                         mm = (MCContextMenuManager) chartCanvas.getContextMenu();
 188                         sash.setOrientation(SWT.HORIZONTAL);
 189                         addActionsToContextMenu(mm);
 190                         // FIXME: The lanes field is initialized by initializeChartConfiguration which is called by the super constructor. This is too indirect for SpotBugs to resolve and should be simplified.
 191                         lanes.updateContextMenu(mm, false);
 192                         form.getToolBarManager()
 193                                         .add(ActionToolkit.action(() -> lanes.openEditLanesDialog(mm, false), Messages.ThreadsPage_EDIT_LANES,
 194                                                         FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_LANES_EDIT)));
 195                         form.getToolBarManager()
 196                                         .add(ActionToolkit.action(() -> openViewThreadDetailsDialog(state), Messages.ThreadsPage_VIEW_THREAD_DETAILS,
 197                                                         FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_TABLE)));
 198                         form.getToolBarManager().update(true);
 199                         chartLegend.getControl().dispose();
 200                         setupFilterBar();
 201                         buildChart();
 202                         chart.setVisibleRange(visibleRange.getStart(), visibleRange.getEnd());
 203                         onFilterChange(tableFilter);
 204                 }
 205 
 206                 private void setupFilterBar() {
 207                         laneFilter = new DropdownLaneFilter(filterBar, lanes, mm);
 208                         laneFilter.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
 209                         filterBar.setChart(chart);
 210                         filterBar.setChartCanvas(chartCanvas);
 211                 }
 212 
 213                 /**
 214                  * Hides a thread from the chart and rebuilds the chart
 215                  */
 216                 private void hideThread(Object thread) {
 217                         if (this.threadRows != null && this.threadRows.size() > 0 && thread instanceof IMCThread) {
 218                                 int index = indexOfThread(thread);
 219                                 if (index != -1) {
 220                                         this.threadRows.remove(index);
 221                                         this.reloadThreads = false;
 222                                         buildChart();
 223                                         if (!this.isChartModified) {
 224                                                 this.isChartModified = true;
 225                                                 setResetChartActionEnablement(true);
 226                                         }
 227                                 }
 228                                 if (this.threadRows.size() == 0) {
 229                                         setHideThreadActionEnablement(false);
 230                                 }
 231                         }
 232                 }
 233 
 234                 /**
 235                  * Locates the index of the target Thread in the current selection list
 236                  *
 237                  * @param thread
 238                  *            the thread of interest
 239                  * @return the index of the thread in the current selection, or -1 if not found
 240                  */
 241                 private int indexOfThread(Object thread) {
 242                         for (int i = 0; i < this.threadRows.size() && thread != null; i++) {
 243                                 if (this.threadRows.get(i) instanceof QuantitySpanRenderer) {
 244                                         if (thread.equals(((QuantitySpanRenderer) this.threadRows.get(i)).getData())) {
 245                                                 return i;
 246                                         }
 247                                 }
 248                         }
 249                         return -1;
 250                 }
 251 
 252                 /**
 253                  * Update the context menu to include actions to hide threads and reset the chart
 254                  */
 255                 private void addActionsToContextMenu(MCContextMenuManager mm) {
 256                         mm.add(new Separator());
 257 
 258                         IAction hideThreadAction = ActionToolkit.action(() -> this.hideThread(chartCanvas.getHoveredItemData()),
 259                                         Messages.ThreadsPage_HIDE_THREAD_ACTION,
 260                                         UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_DELETE));
 261                         hideThreadAction.setId(HIDE_THREAD);
 262                         this.hideThreadAction = hideThreadAction;
 263                         mm.add(hideThreadAction);
 264 
 265                         IAction resetChartAction = ActionToolkit.action(() -> this.resetChartToSelection(),
 266                                         Messages.ThreadsPage_RESET_CHART_TO_SELECTION_ACTION,
 267                                         UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_REFRESH));
 268                         resetChartAction.setId(RESET_CHART);
 269                         resetChartAction.setEnabled(this.isChartModified);
 270                         this.resetChartAction = resetChartAction;
 271                         mm.add(resetChartAction);
 272 
 273                         this.isChartMenuActionsInit = true;
 274                 }
 275 
 276                 /**
 277                  * Redraws the chart, and disables the reset chart menu action
 278                  */
 279                 private void resetChartToSelection() {
 280                         buildChart();
 281                         this.isChartModified = false;
 282                         setResetChartActionEnablement(false);
 283                         setHideThreadActionEnablement(true);
 284                 }
 285 
 286                 private void setHideThreadActionEnablement(Boolean enabled) {
 287                         this.hideThreadAction.setEnabled(enabled);
 288                 }
 289                 private void setResetChartActionEnablement(Boolean enabled) {
 290                         this.resetChartAction.setEnabled(enabled);
 291                 }
 292 
 293                 @Override
 294                 protected ItemHistogram buildHistogram(Composite parent, IState state) {
 295                         ItemHistogram build = HISTOGRAM.buildWithoutBorder(parent, JfrAttributes.EVENT_THREAD,
 296                                         TableSettings.forState(state));
 297                         return build;
 298                 }
 299 
 300                 @Override
 301                 protected IXDataRenderer getChartRenderer(IItemCollection itemsInTable, HistogramSelection tableSelection) {
 302                         List<IXDataRenderer> rows = new ArrayList<>();
 303                         ItemHistogram histogram = getUndisposedTable();
 304                         IItemCollection selectedItems;
 305                         HistogramSelection selection;
 306                         if (tableSelection.getRowCount() == 0) {
 307                                 selectedItems = itemsInTable;
 308                                 selection = histogram.getAllRows();
 309                         } else {
 310                                 selectedItems = tableSelection.getItems();
 311                                 selection = tableSelection;
 312                         }
 313                         boolean useDefaultSelection = rows.size() > 1;
 314                         if (lanes.getLaneDefinitions().stream().anyMatch(a -> a.isEnabled()) && selection.getRowCount() > 0) {
 315                                 if (this.reloadThreads) {
 316                                         this.threadRows = selection
 317                                                         .getSelectedRows((object, items) -> lanes.buildThreadRenderer(object, items))
 318                                                         .collect(Collectors.toList());
 319                                         chartCanvas.setNumItems(this.threadRows.size());
 320                                         textCanvas.setNumItems(this.threadRows.size());
 321                                         this.isChartModified = false;
 322                                         if (this.isChartMenuActionsInit) {
 323                                                 setResetChartActionEnablement(false);
 324                                                 setHideThreadActionEnablement(true);
 325                                         }
 326                                 } else {
 327                                         this.reloadThreads = true;
 328                                 }
 329 
 330                                 double threadsWeight = Math.sqrt(threadRows.size()) * 0.15;
 331                                 double otherRowWeight = Math.max(threadsWeight * 0.1, (1 - threadsWeight) / rows.size());
 332                                 List<Double> weights = Stream
 333                                                 .concat(Stream.generate(() -> otherRowWeight).limit(rows.size()), Stream.of(threadsWeight))
 334                                                 .collect(Collectors.toList());
 335                                 rows.add(RendererToolkit.uniformRows(this.threadRows));
 336                                 useDefaultSelection = true;
 337                                 rows = Arrays.asList(RendererToolkit.weightedRows(rows, weights));
 338                         }
 339                         IXDataRenderer root = rows.size() == 1 ? rows.get(0) : RendererToolkit.uniformRows(rows);
 340                         // We don't use the default selection when there is only one row. This is to get the correct payload.
 341                         return useDefaultSelection ? new ItemRow(root, selectedItems.apply(lanes.getEnabledLanesFilter())) : root;
 342                 }
 343 
 344                 @Override
 345                 protected void onFilterChange(IItemFilter filter) {
 346                         super.onFilterChange(filter);
 347                         tableFilter = filter;
 348                 }
 349 
 350                 @Override
 351                 public void saveTo(IWritableState state) {
 352                         super.saveTo(state);
 353                         saveToLocal();
 354                 }
 355 
 356                 private void saveToLocal() {
 357                         flavorSelectorState = flavorSelector.getFlavorSelectorState();
 358                         histogramSelectionState = getUndisposedTable().getManager().getSelectionState();
 359                         visibleRange = chart.getVisibleRange();
 360                 }
 361 
 362                 @Override
 363                 protected List<IAction> initializeChartConfiguration(IState state) {
 364                         this.isChartMenuActionsInit = false;
 365                         this.isChartModified = false;
 366                         this.reloadThreads = true;
 367                         lanes = new ThreadGraphLanes(() -> getDataSource(), () -> buildChart());
 368                         return lanes.initializeChartConfiguration(Stream.of(state.getChildren(THREAD_LANE)));
 369                 }
 370 
 371                 private TablePopup tablePopup;
 372                 public void openViewThreadDetailsDialog(IState state) {
 373                         tablePopup = new TablePopup(state);
 374                         OnePageWizardDialog.openAndHideCancelButton(tablePopup, 500, 600);
 375                 }
 376 
 377                 private class TablePopup extends WizardPage implements IPerformFinishable {
 378 
 379                         private IState state;
 380 
 381                         protected TablePopup(IState state) {
 382                                 super("ThreadDetailsPage"); //$NON-NLS-1$
 383                                 this.state = state;
 384                                 setTitle(Messages.ThreadsPage_TABLE_POPUP_TITLE);
 385                                 setDescription(Messages.ThreadsPage_TABLE_POPUP_DESCRIPTION);
 386                         }
 387 
 388                         @Override
 389                         public void createControl(Composite parent) {
 390                                 table = buildHistogram(parent, state.getChild(TABLE));
 391                                 MCContextMenuManager mm = MCContextMenuManager.create(table.getManager().getViewer().getControl());
 392                                 ColumnMenusFactory.addDefaultMenus(table.getManager(), mm);
 393                                 table.getManager().getViewer().addSelectionChangedListener(e -> buildChart());
 394                                 table.getManager().getViewer()
 395                                                 .addSelectionChangedListener(e -> pageContainer.showSelection(table.getSelection().getItems()));
 396                                 SelectionStoreActionToolkit.addSelectionStoreActions(pageContainer.getSelectionStore(), table,
 397                                                 NLS.bind(Messages.ChartAndTableUI_HISTOGRAM_SELECTION, getName()), mm);
 398                                 tableFilterComponent = FilterComponent.createFilterComponent(table.getManager().getViewer().getControl(),
 399                                                 table.getManager(), tableFilter, model.getItems().apply(pageFilter),
 400                                                 pageContainer.getSelectionStore()::getSelections, this::onFilterChangeHelper);
 401                                 mm.add(tableFilterComponent.getShowFilterAction());
 402                                 mm.add(tableFilterComponent.getShowSearchAction());
 403                                 table.getManager().setSelectionState(histogramSelectionState);
 404                                 tableFilterComponent.loadState(state.getChild(THREADS_TABLE_FILTER));
 405                                 onFilterChange(tableFilter);
 406 
 407                                 if (selectionInput != null) {
 408                                         table.getManager().getViewer().setSelection(new StructuredSelection(selectionInput));
 409                                 }
 410 
 411                                 Item[] columnWidgets = ((TableViewer) table.getManager().getViewer()).getTable().getColumns();
 412                                 for (Item columWidget : columnWidgets) {
 413                                         columWidget.addListener(SWT.Selection, e -> columnSortChanged());
 414                                 }
 415 
 416                                 setControl(parent);
 417                         }
 418 
 419                         private void columnSortChanged() {
 420                                 if (!table.getSelection().getItems().hasItems()) {
 421                                         buildChart();
 422                                 }
 423                         }
 424 
 425                         private void onFilterChangeHelper(IItemFilter filter) {
 426                                 onFilterChange(filter);
 427                         }
 428 
 429                         @Override
 430                         public boolean performFinish() {
 431                                 IItemCollection lastSelection = table.getSelection().getItems();
 432                                 table.show(lastSelection);
 433                                 selectionInput = (Object[]) table.getManager().getViewer().getInput();
 434                                 return true;
 435                         }
 436                 }
 437         }
 438 
 439         private Object[] selectionInput;
 440         private FlavorSelectorState flavorSelectorState;
 441         private SelectionState histogramSelectionState;
 442         private IItemFilter tableFilter;
 443         private IRange<IQuantity> visibleRange;
 444 
 445         public ThreadsPage(IPageDefinition definition, StreamModel model, IPageContainer editor) {
 446                 super(definition, model, editor);
 447                 visibleRange = editor.getRecordingRange();
 448         }
 449 
 450         @Override
 451         public IPageUI display(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 452                 return new ThreadsPageUi(parent, toolkit, editor, state);
 453         }
 454 
 455 }