=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=/**\r\n * @return the ${bare_field_name}\r\n */ /**\r\n * @param ${param} the ${bare_field_name} to set\r\n */ /**\r\n * ${tags}\r\n */ /**\r\n * \r\n */ /**\r\n * @author ${user}\r\n *\r\n * ${tags}\r\n */ /**\r\n * \r\n */ /**\r\n * ${tags}\r\n */ /* (non-Javadoc)\r\n * ${see_to_overridden}\r\n */ /*******************************************************************************\r\n * Copyright (c) ${year} IBM Corporation and others.\r\n *\n * This program and the accompanying materials\r\n * are made available under the terms of the Eclipse Public License 2.0\r\n * which accompanies this distribution, and is available at\r\n * https\://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier\: EPL-2.0\r\n *\r\n * Contributors\:\r\n * IBM Corporation - initial API and implementation\r\n *******************************************************************************/\r\n${package_declaration}\r\n\r\n${typecomment}\r\n${type_declaration} \r\n \r\n \r\n \r\n // ${todo} Auto-generated method stub\r\n${body_statement} ${body_statement}\r\n// ${todo} Auto-generated constructor stub return ${field}; ${field} \= ${param};
+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 00000000000..a8911549e20
Binary files /dev/null and b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.png differ
diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.svg b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.svg
new file mode 100644
index 00000000000..4b2bf77dda2
--- /dev/null
+++ b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec.svg
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 00000000000..87e738f3243
Binary files /dev/null and b/resources/bundles/org.eclipse.ui.buildmonitor/icons/full/etool16/build_exec@2x.png differ
diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/plugin.properties b/resources/bundles/org.eclipse.ui.buildmonitor/plugin.properties
new file mode 100644
index 00000000000..74c9b4da915
--- /dev/null
+++ b/resources/bundles/org.eclipse.ui.buildmonitor/plugin.properties
@@ -0,0 +1,16 @@
+###############################################################################
+# 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
+###############################################################################
+pluginName=Build Monitor
+providerName=Eclipse.org
+BuildMonitorView.name=Build Monitor
diff --git a/resources/bundles/org.eclipse.ui.buildmonitor/plugin.xml b/resources/bundles/org.eclipse.ui.buildmonitor/plugin.xml
new file mode 100644
index 00000000000..29e67c791da
--- /dev/null
+++ b/resources/bundles/org.eclipse.ui.buildmonitor/plugin.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
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