Skip to content

[improvement](build) Make the FE compilation incremental - #67824

Open
englefly wants to merge 1 commit into
apache:masterfrom
englefly:fe-incremental-compile
Open

[improvement](build) Make the FE compilation incremental#67824
englefly wants to merge 1 commit into
apache:masterfrom
englefly:fe-incremental-compile

Conversation

@englefly

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

A local edit anywhere in fe-core made mvn compile rebuild the whole module: 4473 java
sources, about two minutes. Two independent causes were found and fixed.

1. The pattern hierarchy was regenerated on every build. GeneratedPlanPatterns /
GeneratedMemoPatterns / GeneratedPlanRelations / GeneratedExpressionRelations were
produced by a javac annotation processor which, on every build, parsed all 2678 sources of the
nereids tree with antlr (about 9 s) and unconditionally rewrote the four generated files. The
fresh timestamps kept the whole module stale, and the generated content was not even
reproducible: the file walk order came from FileUtils.listFiles,
PlanTypeMappingGenerator.findSuperPlan collected it into a LinkedHashSet, and
PlanPatternGeneratorAnalyzer read an IdentityHashMap.

Generation is now a plain program (PatternCodeGenerator) run by exec-maven-plugin from
process-sources. It caches the parsed ast of every source file, keyed by its content hash, so
only the modified files are re-parsed, and it rewrites an output file only when its content
really changed, so unchanged generated sources keep their timestamp. The emit order was made
deterministic. Measured: the generation step drops from about 9 s to 0.8 s, and a build with no
source change rewrites 0 of the 4 generated files. Two independent cold runs now produce
byte-identical output.

2. maven-compiler-plugin is all-or-nothing. As soon as one source is newer than its class
file the plugin recompiles every source of the module (Changes detected - recompiling the module!), which is independent of the generated sources: removing the annotation processor
alone does not make an edit incremental.

An opt-in profile -Pfast-fe now adds IncrementalSourceMarker. It indexes the identifiers each
source mentions and the type names it declares, then marks (touches) the changed sources plus
every source which mentions a type they declare. The compiler plugin's per-file mode then
compiles exactly that set. The set is a conservative superset, so no class can be left referring
to a type which changed, moved or disappeared underneath it.

Measured on fe-core, editing one file:

build javac work wall clock
default Compiling 4473 source files about 2:15
-Pfast-fe Compiling 72 source files 27 s

The result was validated by comparing every class file against a forced full rebuild after
changing the inlined constant StringLikeLiteral.CHINESE_CHAR_BYTE_LENGTH, which a broken
dependency propagation would have left stale: all ~6000 class files byte-identical. Editing a
leaf operator marks 130 of 4527 sources, editing a widely used class such as Plan marks 578.

The default build is unchanged; -Pfast-fe is opt-in and intended for the local edit loop. The
only coupling it cannot see is one which is not spelled out in the source at all (reflection,
ServiceLoader, resource lookups), so a release build should keep using the plain build.

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
      • The incremental build was compared byte for byte against a forced full rebuild (all
        ~6000 class files identical) after changing an inlined constant, which a broken
        dependency propagation would have left stale.
      • build-support/tests/test-incremental-source-marker.sh covers the new build tool: no
        change marks nothing, editing a type marks its referrers and not an unrelated file, a
        mention inside a comment or a string literal is not a dependency, a new type marks only
        itself, removing a source deletes its own classes and no others.
      • mvn compile -pl fe-core -am and mvn -Pfast-fe compile -pl fe-core -am both pass with
        0 checkstyle violations.
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

### What problem does this solve?

Problem Summary:

A local edit anywhere in fe-core made `mvn compile` rebuild the whole module: 4487 java files,
about two minutes. Two independent causes were found.

1. The nereids pattern hierarchy (GeneratedPlanPatterns / GeneratedMemoPatterns /
   GeneratedPlanRelations / GeneratedExpressionRelations) was produced by a javac annotation
   processor which, on every build, parsed all 2679 sources of the nereids tree with antlr
   (about 9s) and unconditionally rewrote the four generated files. The fresh timestamps kept
   the whole module stale, and the generated content was not even reproducible: the file walk
   order came from FileUtils.listFiles, PlanTypeMappingGenerator.findSuperPlan collected it into
   a LinkedHashSet, and PlanPatternGeneratorAnalyzer read an IdentityHashMap.

2. maven-compiler-plugin is all-or-nothing by default: as soon as one source is newer than its
   class file it recompiles every source of the module. That is independent of the generated
   sources, so removing the annotation processor alone does not make an edit incremental.

Generation is now a plain program (PatternCodeGenerator) run by exec-maven-plugin from
process-sources: it caches the parsed ast of every source file, keyed by its content hash, so
only the modified files are re-parsed, and it rewrites an output file only when its content
really changed, so unchanged generated sources keep their timestamp. The emit order was made
deterministic. Measured: generation drops from about 9s to 0.8s, and a build with no source
change rewrites 0 of the 4 generated files.

For the second cause, an opt-in profile `-Pfast-fe` adds IncrementalSourceMarker, which indexes
the identifiers each source mentions and the type names it declares, then marks the changed
sources plus every source which mentions a type they declare. The compiler plugin's per-file
mode then compiles exactly that set. The set is a conservative superset, so no class can be left
referring to a type which changed, moved or disappeared underneath it.

Measured on fe-core, editing one file:
  default build : Compiling 4487 source files, about 2:15
  -Pfast-fe     : Compiling 72 source files, 27s

The result was validated by comparing every class file against a forced full rebuild after
changing the inlined constant StringLikeLiteral.CHINESE_CHAR_BYTE_LENGTH: byte-identical.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - The incremental build was compared byte for byte against a forced full rebuild (all
      ~6000 class files identical) after changing an inlined constant, which a broken
      dependency propagation would have left stale.
    - build-support/tests/test-incremental-source-marker.sh covers the new build tool.
    - `mvn compile -pl fe-core -am` and `mvn -Pfast-fe compile -pl fe-core -am` both pass with
      0 checkstyle violations.
- Behavior changed: No, the default build is unchanged and -Pfast-fe is opt-in
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 17063 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit ba26a1c37d27aaddaf55cbdce0dd569e84243d6f, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17594	3080	3058	3058
q2	2092	266	225	225
q3	10236	934	532	532
q4	4670	255	211	211
q5	7661	571	391	391
q6	140	113	95	95
q7	540	498	390	390
q8	9231	872	937	872
q9	3526	2415	2422	2415
q10	6526	858	712	712
q11	399	197	180	180
q12	612	269	199	199
q13	18154	1559	1174	1174
q14	155	149	141	141
q15	q16	426	395	374	374
q17	1301	885	826	826
q18	3133	2305	2265	2265
q19	1297	954	790	790
q20	372	280	204	204
q21	5606	1776	1848	1776
q22	340	278	233	233
Total cold run time: 94011 ms
Total hot run time: 17063 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3417	3361	3362	3361
q2	501	410	381	381
q3	2245	2346	2220	2220
q4	1229	1188	907	907
q5	2253	2168	2122	2122
q6	171	120	88	88
q7	1041	930	869	869
q8	1611	1399	1405	1399
q9	3186	3174	3110	3110
q10	1891	1812	1641	1641
q11	362	271	256	256
q12	457	436	341	341
q13	1477	1540	1178	1178
q14	164	176	169	169
q15	q16	390	402	360	360
q17	3642	3399	3320	3320
q18	4855	4488	4897	4488
q19	946	852	872	852
q20	1026	982	861	861
q21	3945	3245	3254	3245
q22	399	343	319	319
Total cold run time: 35208 ms
Total hot run time: 31487 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 82977 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit ba26a1c37d27aaddaf55cbdce0dd569e84243d6f, data reload: false

query5	4245	413	333	333
query6	377	136	125	125
query7	4961	423	223	223
query8	289	126	118	118
query9	8693	2917	2903	2903
query10	413	221	186	186
query11	5385	1049	940	940
query12	131	73	70	70
query13	1189	458	327	327
query14	6088	2233	2144	2144
query14_1	2019	2015	2011	2011
query15	175	121	114	114
query16	913	387	362	362
query17	787	466	371	371
query18	2335	327	238	238
query19	172	137	117	117
query20	74	71	73	71
query21	202	100	87	87
query22	5405	5494	5356	5356
query23	6974	6447	6277	6277
query23_1	6288	6112	5973	5973
query24	7269	1099	768	768
query24_1	784	788	794	788
query25	440	303	262	262
query26	1235	230	138	138
query27	2785	407	259	259
query28	4678	1511	1509	1509
query29	934	434	356	356
query30	249	164	135	135
query31	838	429	343	343
query32	134	81	76	76
query33	459	233	196	196
query34	977	841	492	492
query35	408	412	349	349
query36	583	564	535	535
query37	121	83	74	74
query38	1016	878	855	855
query39	516	480	484	480
query39_1	489	475	526	475
query40	207	96	78	78
query41	62	56	57	56
query42	79	72	78	72
query43	245	245	213	213
query44	971	541	551	541
query45	108	104	101	101
query46	797	862	539	539
query47	748	758	716	716
query48	327	302	229	229
query49	546	257	226	226
query50	763	267	197	197
query51	8326	8199	8205	8199
query52	114	68	64	64
query53	191	194	157	157
query54	226	171	143	143
query55	77	62	55	55
query56	225	166	178	166
query57	700	628	647	628
query58	196	166	163	163
query59	1240	1230	1113	1113
query60	257	170	176	170
query61	129	129	112	112
query62	340	198	180	180
query63	172	144	134	134
query64	2788	724	577	577
query65	1635	1646	1636	1636
query66	1771	261	196	196
query67	10170	9943	9942	9942
query68	2772	1245	745	745
query69	342	229	209	209
query70	675	614	637	614
query71	249	167	171	167
query72	2297	1748	1255	1255
query73	645	566	352	352
query74	1579	1234	1153	1153
query75	1180	1092	970	970
query76	2291	716	498	498
query77	260	263	212	212
query78	4054	3760	3289	3289
query79	2377	798	588	588
query80	1606	334	290	290
query81	490	155	136	136
query82	619	127	96	96
query83	277	210	188	188
query84	293	111	91	91
query85	768	328	285	285
query86	380	178	167	167
query87	1035	966	904	904
query88	2772	2121	2121	2121
query89	278	199	179	179
query90	2013	130	131	130
query91	130	117	97	97
query92	77	63	69	63
query93	1407	1140	739	739
query94	671	242	230	230
query95	521	255	230	230
query96	792	607	266	266
query97	1086	1062	1044	1044
query98	148	139	134	134
query99	419	349	311	311
Total cold run time: 178351 ms
Total hot run time: 82977 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 14.84 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit ba26a1c37d27aaddaf55cbdce0dd569e84243d6f, data reload: false

query1	0.01	0.01	0.00
query2	0.07	0.04	0.04
query3	0.25	0.11	0.10
query4	1.60	0.09	0.10
query5	0.18	0.17	0.15
query6	1.26	0.69	0.70
query7	0.03	0.01	0.00
query8	0.04	0.03	0.04
query9	0.29	0.22	0.21
query10	0.33	0.34	0.37
query11	0.17	0.12	0.12
query12	0.14	0.13	0.12
query13	0.31	0.32	0.31
query14	0.48	0.46	0.46
query15	0.37	0.36	0.35
query16	0.24	0.23	0.26
query17	0.72	0.73	0.71
query18	0.18	0.18	0.17
query19	1.26	1.19	1.16
query20	0.02	0.01	0.01
query21	15.49	0.17	0.11
query22	5.06	0.04	0.05
query23	16.20	0.26	0.10
query24	3.02	0.32	0.27
query25	0.11	0.04	0.03
query26	0.80	0.16	0.12
query27	0.04	0.02	0.03
query28	3.68	0.55	0.27
query29	12.44	3.20	2.57
query30	0.25	0.13	0.12
query31	2.77	0.39	0.18
query32	3.51	0.33	0.23
query33	1.50	1.50	1.39
query34	15.38	2.23	1.83
query35	1.78	1.77	1.76
query36	0.46	0.29	0.29
query37	0.06	0.03	0.03
query38	0.05	0.04	0.03
query39	0.03	0.03	0.02
query40	0.12	0.07	0.07
query41	0.08	0.03	0.02
query42	0.04	0.02	0.02
query43	0.04	0.03	0.03
Total cold run time: 90.86 s
Total hot run time: 14.84 s

@924060929 924060929 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standalone pattern-generation direction looks reasonable, but the incremental marker is not yet a safe conservative approximation: the cases below can leave stale class files in target/classes. I reproduced each case against this head. Please fix these correctness gaps before merging and add tests that validate the resulting bytecode/runtime behavior, not only which source timestamps were changed.

}
affected.addAll(changed);

// Propagate one hop only: the files which mention a type declared by a changed file. A file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] One-hop propagation is not safe for Java inheritance. For example, let A declare public static final int VALUE = 2, let B extends A, and let C return B.VALUE. After changing A.VALUE to 3, this loop marks only A and B because C mentions B but not A. Recompiling the marked sources leaves C.class with the old inlined value 2; I reproduced this and confirmed iconst_2 with javap. A source that is recompiled without edits can expose a different effective API through its changed parent, so propagation must continue transitively unless ABI comparison proves that it can stop.

}
}

private static boolean isUnder(File file, File directory) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] touchRoot is canonicalized, but scanned files and cache keys use absolute paths. These differ whenever the checkout/root contains a symlink; on macOS the included test already triggers this because mktemp returns /var/folders/... while the canonical path is /private/var/folders/.... isUnder then returns false for every source, so nothing is touched and removed classes are not cleaned. Running test-incremental-source-marker.sh on macOS produced four failures for exactly this reason. Please normalize all roots/files/cache keys consistently and use Path.startsWith rather than string-prefix comparison.

if (parent == null || !parent.isDirectory()) {
return;
}
String simpleName = new File(prefix).getName();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Class cleanup assumes every binary name starts with the source filename. A legal Removed.java containing public class Removed { static class Inner {} } plus package-private top-level class Extra {} produces Removed.class, Removed$Inner.class, and Extra.class; after deleting the source, this code removes the first two but leaves Extra.class. I reproduced that stale class. Since the index already records all declaredNames, cleanup should remove every top-level type declared by the removed source and each type's $ classes; please add this multi-top-level-class case to the test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants