From 4a80ebd539a9f7091d73d4d2e8bc84ad73610bba Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 2 Sep 2026 15:45:23 +0200 Subject: [PATCH] Add a Build Monitor view showing where each build spends its time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding the builder that makes a build slow currently means starting Eclipse with -debug and reading the performance log. This adds an org.eclipse.ui.buildmonitor bundle with a Build Monitor view that records every build and breaks it down, grouped either by project or by builder, with the run count, the time and the share of the build. Tracing is switched on while the view is open and the previous debug options are restored when it closes, so no restart is needed. Each build is bracketed by PRE_BUILD and POST_BUILD and the per builder counters of PerformanceStats are diffed between the two. Those counters are updated while the builder returns, unlike the listener callbacks, which PerformanceStatsProcessor batches with a two second delay and which therefore cannot be attributed to the build that caused them. The build row also carries the wall clock time of the whole build, so the part that is not spent inside builders is visible. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../org.eclipse.ui.buildmonitor/.classpath | 7 + .../org.eclipse.ui.buildmonitor/.project | 34 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.core.runtime.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 141 ++++ .../.settings/org.eclipse.jdt.ui.prefs | 151 +++++ .../META-INF/MANIFEST.MF | 15 + .../org.eclipse.ui.buildmonitor/about.html | 36 + .../build.properties | 22 + .../icons/full/etool16/build_exec.png | Bin 0 -> 602 bytes .../icons/full/etool16/build_exec.svg | 255 +++++++ .../icons/full/etool16/build_exec@2x.png | Bin 0 -> 1128 bytes .../plugin.properties | 16 + .../org.eclipse.ui.buildmonitor/plugin.xml | 14 + .../buildmonitor/BuildMonitorView.java | 629 ++++++++++++++++++ .../ui/internal/buildmonitor/BuildRun.java | 109 +++ .../internal/buildmonitor/BuilderEntry.java | 71 ++ .../internal/buildmonitor/BuilderLabels.java | 75 +++ .../ui/internal/buildmonitor/GroupEntry.java | 64 ++ .../ui/internal/buildmonitor/Messages.java | 57 ++ .../internal/buildmonitor/Messages.properties | 44 ++ 21 files changed, 1744 insertions(+) create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.classpath create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.project create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.resources.prefs create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.runtime.prefs create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.core.prefs create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.ui.prefs create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/META-INF/MANIFEST.MF create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/about.html create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/build.properties create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.png create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.svg create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec@2x.png create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/plugin.properties create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/plugin.xml create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildMonitorView.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildRun.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderEntry.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderLabels.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/GroupEntry.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.java create mode 100644 resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.properties diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.classpath b/resources/bundles/org.eclipse.ui.buildmonitor/.classpath new file mode 100644 index 00000000000..375961e4d61 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.project b/resources/bundles/org.eclipse.ui.buildmonitor/.project new file mode 100644 index 00000000000..05164c25817 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.project @@ -0,0 +1,34 @@ + + + org.eclipse.ui.buildmonitor + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.api.tools.apiAnalysisBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + org.eclipse.pde.api.tools.apiAnalysisNature + + diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.resources.prefs b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.runtime.prefs b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.runtime.prefs new file mode 100644 index 00000000000..5a0ad22d2a7 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.core.runtime.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +line.separator=\n diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.core.prefs b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..70bb1eec7b3 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,141 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.builder.cleanOutputFolder=clean +org.eclipse.jdt.core.builder.duplicateResourceTask=warning +org.eclipse.jdt.core.builder.invalidClasspath=abort +org.eclipse.jdt.core.builder.recreateModifiedClassFileInOutputFolder=ignore +org.eclipse.jdt.core.builder.resourceCopyExclusionFilter=*.launch +org.eclipse.jdt.core.circularClasspath=error +org.eclipse.jdt.core.classpath.exclusionPatterns=enabled +org.eclipse.jdt.core.classpath.multipleOutputLocations=enabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.maxProblemPerUnit=100 +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=error +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=error +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=error +org.eclipse.jdt.core.compiler.problem.emptyStatement=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=error +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=error +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=error +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=error +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=protected +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=error +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=error +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=error +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=error +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=warning +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=error +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=error +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=error +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=error +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=error +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=21 +org.eclipse.jdt.core.compiler.taskCaseSensitive=enabled +org.eclipse.jdt.core.compiler.taskPriorities=NORMAL,NORMAL +org.eclipse.jdt.core.compiler.taskTags=TODO,XXX +org.eclipse.jdt.core.incompatibleJDKLevel=ignore +org.eclipse.jdt.core.incompleteClasspath=error diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.ui.prefs b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000000..83308cb79e5 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,151 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +internal.default.compliance=user +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com; +org.eclipse.jdt.ui.ondemandthreshold=99 +org.eclipse.jdt.ui.staticondemandthreshold=99 +org.eclipse.jdt.ui.text.custom_code_templates= +sp_cleanup.add_all=false +sp_cleanup.add_default_serial_version_id=false +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.also_simplify_lambda=true +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.array_with_curly=false +sp_cleanup.arrays_fill=false +sp_cleanup.bitwise_conditional_expression=false +sp_cleanup.boolean_literal=false +sp_cleanup.boolean_value_rather_than_comparison=true +sp_cleanup.break_loop=false +sp_cleanup.collection_cloning=false +sp_cleanup.comparing_on_criteria=false +sp_cleanup.comparison_statement=false +sp_cleanup.controlflow_merge=false +sp_cleanup.convert_functional_interfaces=true +sp_cleanup.convert_to_enhanced_for_loop=true +sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true +sp_cleanup.convert_to_switch_expressions=false +sp_cleanup.correct_indentation=false +sp_cleanup.do_while_rather_than_while=false +sp_cleanup.double_negation=false +sp_cleanup.else_if=false +sp_cleanup.embedded_if=false +sp_cleanup.evaluate_nullable=false +sp_cleanup.extract_increment=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=true +sp_cleanup.hash=false +sp_cleanup.if_condition=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.instanceof=false +sp_cleanup.instanceof_keyword=false +sp_cleanup.invert_equals=false +sp_cleanup.join=false +sp_cleanup.lazy_logical_operator=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.map_cloning=false +sp_cleanup.merge_conditional_blocks=false +sp_cleanup.multi_catch=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.no_string_creation=false +sp_cleanup.no_super=false +sp_cleanup.number_suffix=false +sp_cleanup.objects_equals=false +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false +sp_cleanup.operand_factorization=false +sp_cleanup.organize_imports=true +sp_cleanup.overridden_assignment=false +sp_cleanup.overridden_assignment_move_decl=true +sp_cleanup.plain_replacement=false +sp_cleanup.precompile_regex=false +sp_cleanup.primitive_comparison=false +sp_cleanup.primitive_parsing=false +sp_cleanup.primitive_rather_than_wrapper=true +sp_cleanup.primitive_serialization=false +sp_cleanup.pull_out_if_from_if_else=false +sp_cleanup.pull_up_assignment=false +sp_cleanup.push_down_negation=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=true +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.reduce_indentation=false +sp_cleanup.redundant_comparator=false +sp_cleanup.redundant_falling_through_block_end=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_modifiers=false +sp_cleanup.remove_redundant_semicolons=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_array_creation=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=true +sp_cleanup.remove_unused_imports=true +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_method_parameters=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.replace_deprecated_calls=false +sp_cleanup.return_expression=false +sp_cleanup.simplify_lambda_expression_and_method_ref=true +sp_cleanup.single_used_field=false +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.standard_comparison=false +sp_cleanup.static_inner_class=false +sp_cleanup.strictly_equal_or_different=false +sp_cleanup.stringbuffer_to_stringbuilder=false +sp_cleanup.stringbuilder=false +sp_cleanup.stringbuilder_for_local_vars=true +sp_cleanup.stringconcat_stringbuffer_stringbuilder=false +sp_cleanup.stringconcat_to_textblock=false +sp_cleanup.substring=false +sp_cleanup.switch=false +sp_cleanup.system_property=false +sp_cleanup.system_property_boolean=false +sp_cleanup.system_property_file_encoding=false +sp_cleanup.system_property_file_separator=false +sp_cleanup.system_property_line_separator=false +sp_cleanup.system_property_path_separator=false +sp_cleanup.ternary_operator=false +sp_cleanup.try_with_resource=false +sp_cleanup.unlooped_while=false +sp_cleanup.unreachable_block=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_autoboxing=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_directly_map_method=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_string_is_blank=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_unboxing=false +sp_cleanup.use_var=false +sp_cleanup.useless_continue=false +sp_cleanup.useless_return=false +sp_cleanup.valueof_rather_than_instantiation=false diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.ui.buildmonitor/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..a42ce3173e2 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.ui.buildmonitor; singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Require-Bundle: org.eclipse.ui;bundle-version="[3.208.0,4.0.0)", + org.eclipse.core.runtime;bundle-version="[3.35.0,4.0.0)", + org.eclipse.core.resources;bundle-version="[3.24.0,4.0.0)" +Import-Package: org.eclipse.osgi.service.debug;version="[1.2.0,2.0.0)" +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Export-Package: org.eclipse.ui.internal.buildmonitor;x-internal:=true +Automatic-Module-Name: org.eclipse.ui.buildmonitor diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/about.html b/resources/bundles/org.eclipse.ui.buildmonitor/about.html new file mode 100644 index 00000000000..164f781a8fd --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/about.html @@ -0,0 +1,36 @@ + + + + +About + + +

About This Content

+ +

November 30, 2017

+

License

+ +

+ The Eclipse Foundation makes available all content in this plug-in + ("Content"). Unless otherwise indicated below, the Content + is provided to you under the terms and conditions of the Eclipse + Public License Version 2.0 ("EPL"). A copy of the EPL is + available at http://www.eclipse.org/legal/epl-2.0. + For purposes of the EPL, "Program" will mean the Content. +

+ +

+ If you did not receive this Content directly from the Eclipse + Foundation, the Content is being redistributed by another party + ("Redistributor") and different terms and conditions may + apply to your use of any object code in the Content. Check the + Redistributor's license that was provided with the Content. If no such + license exists, contact the Redistributor. Unless otherwise indicated + below, the terms and conditions of the EPL still apply to any source + code in the Content and such source code may be obtained at http://www.eclipse.org. +

+ + + \ No newline at end of file diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/build.properties b/resources/bundles/org.eclipse.ui.buildmonitor/build.properties new file mode 100644 index 00000000000..1018bbb7434 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/build.properties @@ -0,0 +1,22 @@ +############################################################################### +# Copyright (c) 2026 Vogella GmbH and others. +# +# This program and the accompanying materials +# are made available under the terms of the Eclipse Public License 2.0 +# which accompanies this distribution, and is available at +# https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Vogella GmbH - initial API and implementation +############################################################################### +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + icons/,\ + plugin.properties,\ + plugin.xml,\ + about.html +src.includes = about.html diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.png b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.png new file mode 100644 index 0000000000000000000000000000000000000000..a8911549e20a7aed235f83afaf54c93d268a824f GIT binary patch literal 602 zcmV-g0;Tl})# zE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec@2x.png b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..87e738f32435b37da5f3c82ab3869603c868de0d GIT binary patch literal 1128 zcmV-u1eg1XP)uTf1oX?F|lcrerqYzE@@NslNQsors^qutY;-G}miCU9~_{ zeG-UyDa;+Z8^a`tD9&gyB|T^|C978@&|pdiF=t)d_gNtKYK9Y(-e283#2c1jib2Ach`IHNJ_clygL`qQI$B*&rrnoXlDU zM2^T+6fhX{AjYSi9!2CT3RG0)f*6~2codPVC{R|B2V!)};Za1cqCjbR0f>=%jz{?G z)(G1F=*7Uu?6ThA_#Cd>7(!R~;IbakJ1~j36w6mz@MLEDaKOqyxW;&}023 zHD2^l;q`4nuwCqt`w@wicx~(V+#^I?5U)34!@GO9w|>)Zc=@QF{?Dnfv=GG5q=Si0 ztLEV^_a{J;UCkmSF)%jA{T%_Z*!)F=e2ADMENz#$_TJ9JNYK~dl@9`G?ey|iZ*(b8 zP*M!y-w6jp;S|z&`8w#@{P_`HTidU4zkhg^f4=D#!gVo69EvUF+DStb&xXYn@%jZ{ z|4^3#dBr6l2FLB_>7S5q{p8dvhwT3$hu4~16YmiSfu*&rUq^E7&%gV4w(J+%;-lJ? z;Zi_fR0`s*6wrC?uKaAp6%>`n>nL3n?-2-rC)N4~MtIg>I{WYr_^66p3S<|SffyLG z`Ap%31k+OgSazl$IZbRY41*|VmRQleTi~k{FY1c{wRcd zo!18O>W6!o9qQwF>8;(;Ux4cN#x3gSTneP*{tTjTcwR=lur{J + + + + + + + diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildMonitorView.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildMonitorView.java new file mode 100644 index 00000000000..87218b0223b --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildMonitorView.java @@ -0,0 +1,629 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.PerformanceStats; +import org.eclipse.core.runtime.ServiceCaller; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.commands.ActionHandler; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.osgi.service.debug.DebugOptions; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.IWorkbenchCommandConstants; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.handlers.IHandlerService; +import org.eclipse.ui.part.ViewPart; + +/** + * Shows what each build cost, grouped either by project or by builder. Builder + * tracing is switched on while the view is open and the previous debug options + * are put back when it closes. + */ +public class BuildMonitorView extends ViewPart { + + /** Performance event recorded once per builder run by org.eclipse.core.resources. */ + private static final String EVENT_BUILDERS = "org.eclipse.core.resources/perf/builders"; //$NON-NLS-1$ + + private static final String OPTION_PERF = "org.eclipse.core.runtime/perf"; //$NON-NLS-1$ + private static final String OPTION_PERF_SUCCESS = "org.eclipse.core.runtime/perf/success"; //$NON-NLS-1$ + + /** + * The options this view turns on, global flag first. A builder threshold of 0 + * records every run without writing any of them to the performance log, and + * perf/success is off because only the per-project counters are read. + */ + private static final List OPTION_ORDER = List.of(OPTION_PERF, OPTION_PERF_SUCCESS, EVENT_BUILDERS); + private static final Map OPTION_VALUES = Map.of(OPTION_PERF, "true", //$NON-NLS-1$ + OPTION_PERF_SUCCESS, "false", //$NON-NLS-1$ + EVENT_BUILDERS, "0"); //$NON-NLS-1$ + + /** + * Bounds on what is kept. One build of a large workspace is thousands of rows + * and one of a single project is a handful, so the row budget is what actually + * bounds the memory and the build count only keeps the top level readable. + */ + private static final int MAX_BUILDS = 25; + private static final int MAX_ROWS = 50_000; + + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss"); //$NON-NLS-1$ + + private static final int COLUMN_NAME = 0; + private static final int COLUMN_RUNS = 1; + private static final int COLUMN_TIME = 2; + private static final int COLUMN_SHARE = 3; + private static final int COLUMN_WALL = 4; + + private final List builds = new ArrayList<>(); + private final Map replacedOptions = new LinkedHashMap<>(); + private boolean tracing; + private boolean debugWasEnabled; + private boolean groupedByBuilder; + + private TreeViewer viewer; + private TreeColumn nameColumn; + private final BuildComparator comparator = new BuildComparator(); + + /** Counters at the moment the running build started, null while none runs. */ + private Map buildStartCounters; + private long buildStartNanos; + private String buildLabel; + private LocalTime buildStarted; + private int buildDepth; + + private final IResourceChangeListener buildListener = event -> { + if (event.getType() == IResourceChangeEvent.PRE_BUILD) { + buildStarted(event); + } else { + buildFinished(); + } + }; + + @Override + public void createPartControl(Composite parent) { + viewer = new TreeViewer(parent, SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); + Tree tree = viewer.getTree(); + tree.setHeaderVisible(true); + tree.setLinesVisible(true); + + nameColumn = addColumn(columnHeader(), null, 420, SWT.LEFT, COLUMN_NAME, this::name); + addColumn(Messages.BuildMonitorView_runs_column, Messages.BuildMonitorView_runs_column_tooltip, 70, SWT.RIGHT, + COLUMN_RUNS, this::runs); + addColumn(Messages.BuildMonitorView_time_column, Messages.BuildMonitorView_time_column_tooltip, 130, SWT.RIGHT, + COLUMN_TIME, this::time); + addColumn(Messages.BuildMonitorView_share_column, Messages.BuildMonitorView_share_column_tooltip, 80, SWT.RIGHT, + COLUMN_SHARE, this::share); + addColumn(Messages.BuildMonitorView_wall_column, Messages.BuildMonitorView_wall_column_tooltip, 160, SWT.RIGHT, + COLUMN_WALL, this::wall); + + viewer.setContentProvider(new BuildContentProvider()); + viewer.setComparator(comparator); + viewer.setInput(builds); + + createContextMenu(); + createToolBar(); + + tracing = enableTracing(); + if (tracing) { + ResourcesPlugin.getWorkspace().addResourceChangeListener(buildListener, + IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.POST_BUILD); + } + updateDescription(); + } + + private void createToolBar() { + IToolBarManager toolBar = getViewSite().getActionBars().getToolBarManager(); + + Action byBuilder = new Action(Messages.BuildMonitorView_by_builder_action, IAction.AS_CHECK_BOX) { + @Override + public void run() { + groupedByBuilder = isChecked(); + nameColumn.setText(columnHeader()); + withoutRedraw(viewer::refresh); + } + }; + byBuilder.setToolTipText(Messages.BuildMonitorView_by_builder_action_tooltip); + toolBar.add(byBuilder); + // also in the view menu, where Eclipse puts a grouping switch + getViewSite().getActionBars().getMenuManager().add(byBuilder); + getViewSite().getActionBars().getMenuManager().add(copyAction()); + toolBar.add(new Separator()); + toolBar.add(action(Messages.BuildMonitorView_expand_all_action, Messages.BuildMonitorView_expand_all_action, + ISharedImages.IMG_ELCL_EXPANDALL, () -> withoutRedraw(viewer::expandAll))); + toolBar.add(action(Messages.BuildMonitorView_collapse_all_action, Messages.BuildMonitorView_collapse_all_action, + ISharedImages.IMG_ELCL_COLLAPSEALL, () -> withoutRedraw(viewer::collapseAll))); + toolBar.add(new Separator()); + toolBar.add(action(Messages.BuildMonitorView_clear_action, Messages.BuildMonitorView_clear_action_tooltip, + ISharedImages.IMG_ETOOL_CLEAR, () -> { + builds.clear(); + viewer.refresh(); + updateDescription(); + })); + } + + @Override + public void dispose() { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(buildListener); + disableTracing(); + super.dispose(); + } + + @Override + public void setFocus() { + viewer.getControl().setFocus(); + } + + private void createContextMenu() { + Action copy = copyAction(); + MenuManager menuManager = new MenuManager(); + menuManager.add(copy); + Menu menu = menuManager.createContextMenu(viewer.getTree()); + viewer.getTree().setMenu(menu); + + // Ctrl+C while the tree has focus. The retargetable action alone is not + // enough, the copy command is answered by another handler otherwise. + getViewSite().getActionBars().setGlobalActionHandler(ActionFactory.COPY.getId(), copy); + getViewSite().getActionBars().updateActionBars(); + IHandlerService handlers = getSite().getService(IHandlerService.class); + if (handlers != null) { + handlers.activateHandler(IWorkbenchCommandConstants.EDIT_COPY, new ActionHandler(copy)); + } + } + + private Action copyAction() { + Action copy = new Action(Messages.BuildMonitorView_copy_action) { + @Override + public void run() { + copyToClipboard(); + } + }; + copy.setToolTipText(Messages.BuildMonitorView_copy_action_tooltip); + return copy; + } + + /** Copies the selected rows, or everything when nothing is selected. */ + private void copyToClipboard() { + List rows = new ArrayList<>(); + if (viewer.getSelection() instanceof IStructuredSelection selection && !selection.isEmpty()) { + selection.forEach(rows::add); + } else { + for (BuildRun build : builds) { + rows.add(build); + for (GroupEntry group : build.getGroups(groupedByBuilder)) { + rows.add(group); + rows.addAll(group.getChildren()); + } + } + } + if (rows.isEmpty()) { + return; + } + StringBuilder text = new StringBuilder(Messages.BuildMonitorView_copy_header); + rows.forEach(row -> appendRow(text, row)); + Clipboard clipboard = new Clipboard(viewer.getTree().getDisplay()); + try { + clipboard.setContents(new Object[] { text.toString() }, + new Transfer[] { TextTransfer.getInstance() }); + } finally { + clipboard.dispose(); + } + } + + /** + * One tab separated line per row. Every row carries its build, project and + * builder, so a line stays meaningful once it is out of the tree. + */ + private void appendRow(StringBuilder text, Object element) { + String build = ""; //$NON-NLS-1$ + String started = ""; //$NON-NLS-1$ + String project = ""; //$NON-NLS-1$ + String builder = ""; //$NON-NLS-1$ + if (element instanceof BuildRun run) { + build = run.getLabel(); + started = TIME.format(run.getStarted()); + } else if (element instanceof GroupEntry group) { + if (group.isGroupedByBuilder()) { + builder = group.getName(); + } else { + project = group.getName(); + } + } else if (element instanceof BuilderEntry leaf) { + project = leaf.getProject(); + builder = BuilderLabels.of(leaf.getBuilder()); + } + text.append(System.lineSeparator()); + text.append(clean(build)).append('\t').append(started).append('\t'); + text.append(clean(project)).append('\t').append(clean(builder)).append('\t'); + text.append(runsOf(element)).append('\t').append(timeOf(element)).append('\t'); + text.append(String.format("%.1f", Double.valueOf(shareOf(element)))).append('\t'); //$NON-NLS-1$ + text.append(element instanceof BuildRun run ? Long.toString(run.getWallTime()) : ""); //$NON-NLS-1$ + } + + private static String clean(String value) { + return value.replace('\t', ' ').replace('\n', ' '); + } + + private String columnHeader() { + return groupedByBuilder ? Messages.BuildMonitorView_builder_column : Messages.BuildMonitorView_project_column; + } + + private static Action action(String text, String tooltip, String sharedImage, Runnable body) { + Action action = new Action(text) { + @Override + public void run() { + body.run(); + } + }; + action.setToolTipText(tooltip); + action.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(sharedImage)); + return action; + } + + /** + * Expanding a build of a large workspace creates thousands of rows, so the tree + * is only painted once the whole operation is done. + */ + private void withoutRedraw(Runnable body) { + Tree tree = viewer.getTree(); + tree.setRedraw(false); + try { + BusyIndicator.showWhile(tree.getDisplay(), body); + } finally { + tree.setRedraw(true); + } + } + + private TreeColumn addColumn(String header, String tooltip, int width, int alignment, int index, + Function text) { + TreeViewerColumn column = new TreeViewerColumn(viewer, alignment); + TreeColumn treeColumn = column.getColumn(); + treeColumn.setText(header); + treeColumn.setToolTipText(tooltip); + treeColumn.setWidth(width); + treeColumn.addListener(SWT.Selection, e -> sortBy(index, treeColumn)); + column.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + return text.apply(element); + } + }); + return treeColumn; + } + + /** Clicking the sorted column flips the direction, another column starts at descending. */ + private void sortBy(int column, TreeColumn treeColumn) { + if (comparator.column == column) { + comparator.ascending = !comparator.ascending; + } else { + comparator.column = column; + comparator.ascending = column == COLUMN_NAME; + } + Tree tree = viewer.getTree(); + tree.setSortColumn(treeColumn); + tree.setSortDirection(comparator.ascending ? SWT.UP : SWT.DOWN); + withoutRedraw(viewer::refresh); + } + + private String name(Object element) { + if (element instanceof BuildRun build) { + String pattern = build.getProjects() == 1 ? Messages.BuildMonitorView_build_label_one + : Messages.BuildMonitorView_build_label; + return NLS.bind(pattern, new Object[] { build.getLabel(), TIME.format(build.getStarted()), + Integer.valueOf(build.getProjects()) }); + } + if (element instanceof GroupEntry group) { + return group.getName(); + } + return ((BuilderEntry) element).getLabel(groupedByBuilder); + } + + private String runs(Object element) { + return Integer.toString(runsOf(element)); + } + + private String time(Object element) { + return Long.toString(timeOf(element)); + } + + /** + * On a build, how much of it was builders at all; below it, the row's share of + * that build's builder time. + */ + private String share(Object element) { + return String.format("%.1f%%", Double.valueOf(shareOf(element))); //$NON-NLS-1$ + } + + private static double shareOf(Object element) { + if (element instanceof BuildRun build) { + return build.getShare(); + } + if (element instanceof GroupEntry group) { + return group.getShare(); + } + return ((BuilderEntry) element).getShare(); + } + + /** Only a build has a wall time; below it the builder times are the whole story. */ + private String wall(Object element) { + return element instanceof BuildRun build ? Long.toString(build.getWallTime()) : ""; //$NON-NLS-1$ + } + + private static int runsOf(Object element) { + if (element instanceof BuildRun build) { + return build.getRuns(); + } + if (element instanceof GroupEntry group) { + return group.getRuns(); + } + return ((BuilderEntry) element).getRuns(); + } + + private static long timeOf(Object element) { + if (element instanceof BuildRun build) { + return build.getTime(); + } + if (element instanceof GroupEntry group) { + return group.getTime(); + } + return ((BuilderEntry) element).getTime(); + } + + private void buildStarted(IResourceChangeEvent event) { + if (buildDepth++ > 0) { + // a builder that builds another project nests, and the outer build owns the time + return; + } + buildStartCounters = counters(); + buildStartNanos = System.nanoTime(); + buildStarted = LocalTime.now(); + buildLabel = label(event); + } + + private void buildFinished() { + if (buildDepth > 0) { + buildDepth--; + } + if (buildDepth > 0 || buildStartCounters == null) { + return; + } + long wallTime = (System.nanoTime() - buildStartNanos) / 1_000_000L; + List leaves = since(buildStartCounters); + buildStartCounters = null; + if (leaves.isEmpty()) { + // a build that ran no builder at all is noise rather than a measurement + return; + } + BuildRun build = new BuildRun(buildLabel, buildStarted, wallTime, leaves); + Tree tree = viewer.getTree(); + if (tree.isDisposed()) { + return; + } + tree.getDisplay().asyncExec(() -> add(build)); + } + + private void add(BuildRun build) { + if (viewer.getTree().isDisposed()) { + return; + } + builds.add(0, build); + trim(); + viewer.refresh(); + viewer.expandToLevel(build, 1); + viewer.reveal(build); + updateDescription(); + } + + /** Drops the oldest builds until both bounds hold, always keeping the newest. */ + private void trim() { + int rows = 0; + for (BuildRun build : builds) { + rows += build.getLeafCount(); + } + while (builds.size() > 1 && (builds.size() > MAX_BUILDS || rows > MAX_ROWS)) { + rows -= builds.remove(builds.size() - 1).getLeafCount(); + } + } + + /** + * The accumulated builder counters, keyed by builder and project. + * {@link PerformanceStats} updates these while the builder returns, so they are + * complete by the time the build ends, unlike the batched listener callbacks. + */ + private static Map counters() { + Map counters = new HashMap<>(); + for (PerformanceStats stats : PerformanceStats.getAllStats()) { + if (EVENT_BUILDERS.equals(stats.getEvent()) && stats.getContext() != null) { + counters.put(new Key(stats.getBlameString(), stats.getContext()), + new long[] { stats.getRunCount(), stats.getRunningTime() }); + } + } + return counters; + } + + /** What the counters gained since the given snapshot. */ + private static List since(Map before) { + List leaves = new ArrayList<>(); + counters().forEach((key, now) -> { + long[] then = before.getOrDefault(key, new long[] { 0, 0 }); + int runs = (int) (now[0] - then[0]); + if (runs > 0) { + leaves.add(new BuilderEntry(key.builder(), key.project(), runs, Math.max(now[1] - then[1], 0))); + } + }); + return leaves; + } + + /** + * A builder on a project. The two strings belong to the shared + * {@link PerformanceStats} objects, so every recorded build refers to the same + * names rather than to copies of them. + */ + private record Key(String builder, String project) { + } + + private static String label(IResourceChangeEvent event) { + String kind = switch (event.getBuildKind()) { + case IncrementalProjectBuilder.FULL_BUILD -> Messages.BuildMonitorView_kind_full; + case IncrementalProjectBuilder.CLEAN_BUILD -> Messages.BuildMonitorView_kind_clean; + case IncrementalProjectBuilder.AUTO_BUILD -> Messages.BuildMonitorView_kind_auto; + default -> Messages.BuildMonitorView_kind_incremental; + }; + if (event.getSource() instanceof IProject project) { + return NLS.bind(Messages.BuildMonitorView_scope_project, kind, project.getName()); + } + return NLS.bind(Messages.BuildMonitorView_scope_workspace, kind); + } + + private void updateDescription() { + if (!tracing) { + setContentDescription(Messages.BuildMonitorView_tracing_unavailable); + } else if (builds.isEmpty()) { + setContentDescription(Messages.BuildMonitorView_waiting); + } else { + setContentDescription(NLS.bind(Messages.BuildMonitorView_recorded, Integer.valueOf(builds.size()))); + } + } + + /** + * Turns builder tracing on, remembering the previous state so that + * {@link #disableTracing()} can put it back. Returns whether the debug options + * service was available. + */ + private boolean enableTracing() { + return ServiceCaller.callOnce(getClass(), DebugOptions.class, options -> { + debugWasEnabled = options.isDebugEnabled(); + for (String option : OPTION_ORDER) { + replacedOptions.put(option, options.getOption(option)); + } + options.setDebugEnabled(true); + for (String option : OPTION_ORDER) { + options.setOption(option, OPTION_VALUES.get(option)); + } + }); + } + + private void disableTracing() { + if (!tracing) { + // nothing was changed, so there is nothing to put back + return; + } + tracing = false; + ServiceCaller.callOnce(getClass(), DebugOptions.class, options -> { + // Put the options back before disabling. Equinox stashes whatever is set when + // debugging goes off and hands it out again the next time it goes on, so + // leaving ours in place would resurrect them. + replacedOptions.forEach((option, value) -> { + if (value == null) { + options.removeOption(option); + } else { + options.setOption(option, value); + } + }); + if (!debugWasEnabled) { + options.setDebugEnabled(false); + } + }); + } + + /** + * Sorts by the column whose header was clicked. Without a chosen column the + * builds stay newest first and everything below them slowest first. + */ + private final class BuildComparator extends ViewerComparator { + + private int column = -1; + private boolean ascending; + + @Override + public int compare(Viewer v, Object e1, Object e2) { + if (column < 0) { + return e1 instanceof BuildRun ? 0 : Long.compare(timeOf(e2), timeOf(e1)); + } + int result = switch (column) { + case COLUMN_RUNS -> Integer.compare(runsOf(e1), runsOf(e2)); + case COLUMN_TIME -> Long.compare(timeOf(e1), timeOf(e2)); + case COLUMN_SHARE -> Double.compare(shareOf(e1), shareOf(e2)); + case COLUMN_WALL -> Long.compare(wallOf(e1), wallOf(e2)); + default -> name(e1).compareToIgnoreCase(name(e2)); + }; + return ascending ? result : -result; + } + + private long wallOf(Object element) { + return element instanceof BuildRun build ? build.getWallTime() : 0; + } + } + + private final class BuildContentProvider implements ITreeContentProvider { + + @Override + public Object[] getElements(Object input) { + return ((List) input).toArray(); + } + + @Override + public Object[] getChildren(Object element) { + if (element instanceof BuildRun build) { + return build.getGroups(groupedByBuilder).toArray(); + } + if (element instanceof GroupEntry group) { + return group.getChildren().toArray(); + } + return new Object[0]; + } + + @Override + public boolean hasChildren(Object element) { + return element instanceof BuildRun || element instanceof GroupEntry; + } + + @Override + public Object getParent(Object element) { + return null; + } + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildRun.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildRun.java new file mode 100644 index 00000000000..11f41295b1c --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuildRun.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * One build invocation, with the time each builder spent on each project in it. + * The same leaves are grouped either by project or by builder. + */ +class BuildRun { + + private final String label; + private final LocalTime started; + private final long wallTime; + private final List leaves; + private final long builderTime; + private final int runs; + private final int projects; + + private List byProject; + private List byBuilder; + + BuildRun(String label, LocalTime started, long wallTime, List leaves) { + this.label = label; + this.started = started; + this.wallTime = wallTime; + this.leaves = leaves; + this.builderTime = leaves.stream().mapToLong(BuilderEntry::getTime).sum(); + this.runs = leaves.stream().mapToInt(BuilderEntry::getRuns).sum(); + this.projects = (int) leaves.stream().map(BuilderEntry::getProject).distinct().count(); + leaves.forEach(leaf -> leaf.setBuildTotal(builderTime)); + } + + /** How many projects actually ran a builder in this build. */ + int getProjects() { + return projects; + } + + /** How much of the build was spent inside builders rather than around them. */ + double getShare() { + return wallTime > 0 ? 100.0 * builderTime / wallTime : 0; + } + + String getLabel() { + return label; + } + + LocalTime getStarted() { + return started; + } + + /** Time from the start of the build until every builder has finished. */ + long getWallTime() { + return wallTime; + } + + /** Time spent inside builders, which is at most {@link #getWallTime()}. */ + long getTime() { + return builderTime; + } + + int getRuns() { + return runs; + } + + /** How many rows this build contributes to the tree once fully expanded. */ + int getLeafCount() { + return leaves.size(); + } + + synchronized List getGroups(boolean groupedByBuilder) { + if (groupedByBuilder) { + if (byBuilder == null) { + byBuilder = group(true); + } + return byBuilder; + } + if (byProject == null) { + byProject = group(false); + } + return byProject; + } + + private List group(boolean groupedByBuilder) { + Map> children = new LinkedHashMap<>(); + for (BuilderEntry leaf : leaves) { + children.computeIfAbsent(leaf.getGroup(groupedByBuilder), name -> new ArrayList<>()).add(leaf); + } + List groups = new ArrayList<>(); + children.forEach((name, entries) -> groups.add(new GroupEntry(name, entries, groupedByBuilder, builderTime))); + return groups; + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderEntry.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderEntry.java new file mode 100644 index 00000000000..9ff90a59726 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderEntry.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +/** + * What one builder cost on one project in one build. This is the leaf of the + * tree either way round, so it carries both names and the grouping decides + * which of them is shown. + */ +class BuilderEntry { + + private final String builder; + private final String project; + private final int runs; + private final long time; + private long buildTotal; + + BuilderEntry(String builder, String project, int runs, long time) { + this.builder = builder; + this.project = project; + this.runs = runs; + this.time = time; + } + + /** The builder class name, which is what the performance event blames. */ + String getBuilder() { + return builder; + } + + String getProject() { + return project; + } + + int getRuns() { + return runs; + } + + long getTime() { + return time; + } + + /** Set once by the enclosing build, which only knows its total after all leaves exist. */ + void setBuildTotal(long buildTotal) { + this.buildTotal = buildTotal; + } + + /** This leaf's percentage of the builder time of its build. */ + double getShare() { + return buildTotal > 0 ? 100.0 * time / buildTotal : 0; + } + + /** The name of the dimension the enclosing group does not already show. */ + String getLabel(boolean groupedByBuilder) { + return groupedByBuilder ? project : BuilderLabels.of(builder); + } + + String getGroup(boolean groupedByBuilder) { + return groupedByBuilder ? BuilderLabels.of(builder) : project; + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderLabels.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderLabels.java new file mode 100644 index 00000000000..a386bcecb6c --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/BuilderLabels.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +import java.util.HashMap; +import java.util.Map; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtension; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.Platform; + +/** + * Maps a builder class name to the name its extension declares, so the tree can + * show "Java Builder" rather than the implementation class. + */ +final class BuilderLabels { + + private static Map namesByClass; + + private BuilderLabels() { + // Do not instantiate. + } + + /** The declared name of the builder, or the class name when there is none. */ + static synchronized String of(String builderClass) { + if (namesByClass == null) { + namesByClass = read(); + } + return namesByClass.getOrDefault(builderClass, builderClass); + } + + private static Map read() { + Map names = new HashMap<>(); + IExtensionPoint point = Platform.getExtensionRegistry() + .getExtensionPoint(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_BUILDERS); + if (point == null) { + return names; + } + for (IExtension extension : point.getExtensions()) { + String name = extension.getLabel(); + if (name == null || name.isBlank()) { + continue; + } + for (IConfigurationElement element : extension.getConfigurationElements()) { + collectRunClasses(element, name, names); + } + } + return names; + } + + private static void collectRunClasses(IConfigurationElement element, String name, Map names) { + if ("run".equals(element.getName())) { //$NON-NLS-1$ + String className = element.getAttribute("class"); //$NON-NLS-1$ + if (className != null) { + names.putIfAbsent(className, name); + } + return; + } + for (IConfigurationElement child : element.getChildren()) { + collectRunClasses(child, name, names); + } + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/GroupEntry.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/GroupEntry.java new file mode 100644 index 00000000000..12a775ba555 --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/GroupEntry.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +import java.util.List; + +/** + * The middle level of the tree: one project with its builders below it, or one + * builder with its projects below it, depending on the grouping. + */ +class GroupEntry { + + private final String name; + private final List children; + private final boolean groupedByBuilder; + private final long time; + private final int runs; + private final long buildTotal; + + GroupEntry(String name, List children, boolean groupedByBuilder, long buildTotal) { + this.name = name; + this.children = children; + this.groupedByBuilder = groupedByBuilder; + this.buildTotal = buildTotal; + this.time = children.stream().mapToLong(BuilderEntry::getTime).sum(); + this.runs = children.stream().mapToInt(BuilderEntry::getRuns).sum(); + } + + /** This group's percentage of the builder time of its build. */ + double getShare() { + return buildTotal > 0 ? 100.0 * time / buildTotal : 0; + } + + String getName() { + return name; + } + + boolean isGroupedByBuilder() { + return groupedByBuilder; + } + + long getTime() { + return time; + } + + int getRuns() { + return runs; + } + + List getChildren() { + return children; + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.java b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.java new file mode 100644 index 00000000000..4f3d81df9ae --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vogella GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.buildmonitor; + +import org.eclipse.osgi.util.NLS; + +final class Messages extends NLS { + public static String BuildMonitorView_build_label; + public static String BuildMonitorView_build_label_one; + public static String BuildMonitorView_builder_column; + public static String BuildMonitorView_by_builder_action; + public static String BuildMonitorView_by_builder_action_tooltip; + public static String BuildMonitorView_clear_action; + public static String BuildMonitorView_copy_action; + public static String BuildMonitorView_copy_action_tooltip; + public static String BuildMonitorView_copy_header; + public static String BuildMonitorView_clear_action_tooltip; + public static String BuildMonitorView_collapse_all_action; + public static String BuildMonitorView_expand_all_action; + public static String BuildMonitorView_kind_auto; + public static String BuildMonitorView_kind_clean; + public static String BuildMonitorView_kind_full; + public static String BuildMonitorView_kind_incremental; + public static String BuildMonitorView_project_column; + public static String BuildMonitorView_recorded; + public static String BuildMonitorView_runs_column; + public static String BuildMonitorView_share_column; + public static String BuildMonitorView_share_column_tooltip; + public static String BuildMonitorView_runs_column_tooltip; + public static String BuildMonitorView_scope_project; + public static String BuildMonitorView_scope_workspace; + public static String BuildMonitorView_time_column; + public static String BuildMonitorView_time_column_tooltip; + public static String BuildMonitorView_tracing_unavailable; + public static String BuildMonitorView_waiting; + public static String BuildMonitorView_wall_column; + public static String BuildMonitorView_wall_column_tooltip; + + private Messages() { + // Do not instantiate. + } + + static { + NLS.initializeMessages(Messages.class.getName(), Messages.class); + } +} diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.properties b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.properties new file mode 100644 index 00000000000..802ed427f4c --- /dev/null +++ b/resources/bundles/org.eclipse.ui.buildmonitor/src/org/eclipse/ui/internal/buildmonitor/Messages.properties @@ -0,0 +1,44 @@ +############################################################################### +# Copyright (c) 2026 Vogella GmbH and others. +# +# This program and the accompanying materials +# are made available under the terms of the Eclipse Public License 2.0 +# which accompanies this distribution, and is available at +# https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Vogella GmbH - initial API and implementation +############################################################################### + +BuildMonitorView_build_label={0} at {1} for {2} projects +BuildMonitorView_build_label_one={0} at {1} for 1 project +BuildMonitorView_builder_column=Build / Builder / Project +BuildMonitorView_by_builder_action=By builder +BuildMonitorView_by_builder_action_tooltip=Group each build by builder instead of by project +BuildMonitorView_clear_action=Clear +BuildMonitorView_copy_action=Copy +BuildMonitorView_copy_action_tooltip=Copies the selected rows, or all of them, as tab separated text +BuildMonitorView_copy_header=Build\tStarted\tProject\tBuilder\tRuns\tBuilder Time (ms)\tShare (%)\tTotal Build Time (ms) +BuildMonitorView_clear_action_tooltip=Discards the recorded builds +BuildMonitorView_collapse_all_action=Collapse All +BuildMonitorView_expand_all_action=Expand All +BuildMonitorView_kind_auto=Auto build +BuildMonitorView_kind_clean=Clean +BuildMonitorView_kind_full=Full build +BuildMonitorView_kind_incremental=Incremental build +BuildMonitorView_project_column=Build / Project / Builder +BuildMonitorView_recorded={0} builds recorded, newest first. +BuildMonitorView_runs_column=Runs +BuildMonitorView_share_column=Share +BuildMonitorView_share_column_tooltip=Share of this build's builder time; on a build row, how much of the build was builders at all +BuildMonitorView_runs_column_tooltip=How often a builder was invoked +BuildMonitorView_scope_project={0} of {1} +BuildMonitorView_scope_workspace={0} of the workspace +BuildMonitorView_time_column=Builder Time (ms) +BuildMonitorView_time_column_tooltip=Time spent inside the builders themselves +BuildMonitorView_tracing_unavailable=Tracing is unavailable, the debug options service was not found. +BuildMonitorView_waiting=Builder tracing is on. Run a build to see where its time goes. +BuildMonitorView_wall_column=Total Build Time (ms) +BuildMonitorView_wall_column_tooltip=Time the whole build took, including the work around the builders