Skip to content

Commit 6e93b74

Browse files
committed
#558 WWZ: harden threading/cancellation and add MT tests
Address review follow-ups by making WWZ cancellation semantics explicit (including cleared results), null-guarding plugin interrupt and thread-count extraction, keeping dialog defaults synced to recommended threads, and updating stale thread-count docs. Add multi-thread consistency, cancellation, and scaling tests, and rerun WWZ UT/benchmark coverage. Acknowledgement: I implemented all of this with assistance from Cursor AI. Made-with: Cursor
1 parent 11feb9f commit 6e93b74

7 files changed

Lines changed: 275 additions & 7 deletions

File tree

src/org/aavso/tools/vstar/plugin/period/impl/WeightedWaveletZTransformPluginBase.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ protected List<ITextComponent<?>> createNumberFields(DoubleField... moreFields)
7070
currTimeDivisions);
7171
fields.add(timeDivisionsField);
7272

73+
int recommendedThreads = WeightedWaveletZTransform.getRecommendedThreadCount();
74+
if (currThreadCount == null || currThreadCount > recommendedThreads) {
75+
currThreadCount = recommendedThreads;
76+
}
7377
threadCountField = new IntegerField(LocaleProps.get("WWZ_PARAMETERS_THREADS"),
74-
1, Math.max(1, Runtime.getRuntime().availableProcessors()), currThreadCount);
78+
1, recommendedThreads, currThreadCount);
7579
fields.add(threadCountField);
7680

7781
return fields;
@@ -100,6 +104,8 @@ public void reset() {
100104
*/
101105
@Override
102106
public void interrupt() {
103-
wwt.interrupt();
107+
if (wwt != null) {
108+
wwt.interrupt();
109+
}
104110
}
105111
}

src/org/aavso/tools/vstar/plugin/period/impl/WeightedWaveletZTransformWithFrequencyRangePlugin.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ public void executeAlgorithm(List<ValidObservation> obs)
8888
currDeltaFreq = deltaFreq = deltaFreqField.getValue();
8989
currDecay = decay = decayField.getValue();
9090
currTimeDivisions = timeDivisions = timeDivisionsField.getValue();
91-
currThreadCount = threadCount = threadCountField.getValue();
91+
Integer threadCountValue = threadCountField.getValue();
92+
if (threadCountValue == null) {
93+
threadCountValue = WeightedWaveletZTransform.getRecommendedThreadCount();
94+
}
95+
currThreadCount = threadCount = threadCountValue;
9296

9397
// TODO: ask about number of frequencies > 1000 via dialog?
9498

@@ -97,6 +101,10 @@ public void executeAlgorithm(List<ValidObservation> obs)
97101
wwt.make_freqs_from_freq_range(Math.min(minFreq, maxFreq), Math
98102
.max(minFreq, maxFreq), deltaFreq);
99103
wwt.execute();
104+
if (wwt.isCancelled()) {
105+
throw new CancellationException("WWZ "
106+
+ LocaleProps.get("CANCELLED"));
107+
}
100108
} else {
101109
throw new CancellationException("WWZ "
102110
+ LocaleProps.get("CANCELLED"));

src/org/aavso/tools/vstar/plugin/period/impl/WeightedWaveletZTransformWithPeriodRangePlugin.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ public void executeAlgorithm(List<ValidObservation> obs)
8585
currDeltaPeriod = deltaPeriod = deltaPeriodField.getValue();
8686
currDecay = decay = decayField.getValue();
8787
currTimeDivisions = timeDivisions = timeDivisionsField.getValue();
88-
currThreadCount = threadCount = threadCountField.getValue();
88+
Integer threadCountValue = threadCountField.getValue();
89+
if (threadCountValue == null) {
90+
threadCountValue = WeightedWaveletZTransform.getRecommendedThreadCount();
91+
}
92+
currThreadCount = threadCount = threadCountValue;
8993

9094
// TODO: ask about number of periods > 1000 via dialog?
9195

@@ -94,6 +98,10 @@ public void executeAlgorithm(List<ValidObservation> obs)
9498
wwt.make_freqs_from_period_range(Math.min(minPeriod, maxPeriod),
9599
Math.max(minPeriod, maxPeriod), deltaPeriod);
96100
wwt.execute();
101+
if (wwt.isCancelled()) {
102+
throw new CancellationException("WWZ "
103+
+ LocaleProps.get("CANCELLED"));
104+
}
97105
} else {
98106
throw new CancellationException("WWZ "
99107
+ LocaleProps.get("CANCELLED"));

src/org/aavso/tools/vstar/util/period/wwz/WeightedWaveletZTransform.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public class WeightedWaveletZTransform implements IAlgorithm {
9595
private double tau[];
9696

9797
private volatile boolean interrupted;
98+
private volatile boolean cancelled;
9899
private int threadCount;
99100

100101
/**
@@ -132,6 +133,7 @@ public WeightedWaveletZTransform(List<ValidObservation> observations,
132133
threadCount = MAX_AVAILABLE_THREADS;
133134

134135
interrupted = false;
136+
cancelled = false;
135137
}
136138

137139
/**
@@ -161,11 +163,18 @@ public List<ValidObservation> getObs() {
161163
@Override
162164
public void execute() throws AlgorithmError {
163165
interrupted = false;
166+
cancelled = false;
164167
try {
165168
wwt();
166169
computeMinAndMaxValues();
167170
} catch (InterruptedException e) {
168-
// Do nothing; just return.
171+
cancelled = true;
172+
stats = new ArrayList<WWZStatistic>();
173+
maximalStats = new ArrayList<WWZStatistic>();
174+
} catch (RuntimeException e) {
175+
stats = new ArrayList<WWZStatistic>();
176+
maximalStats = new ArrayList<WWZStatistic>();
177+
throw new AlgorithmError(e.getMessage() != null ? e.getMessage() : "WWZ runtime failure");
169178
}
170179
}
171180

@@ -176,8 +185,9 @@ public void interrupt() {
176185
/**
177186
* Number of threads (cores) to use for WWZ execution.
178187
* <p>
179-
* Currently this is a configuration hook for future/optional parallel execution
180-
* and UI integration. Values are clamped to [1, maxAvailableThreads].
188+
* This controls threaded WWZ execution. Values are clamped to
189+
* [1, maxAvailableThreads]. A workload heuristic may still run effectively
190+
* single-threaded for small jobs.
181191
* </p>
182192
*
183193
* @param threadCount desired number of threads/cores
@@ -199,6 +209,13 @@ public int getThreadCount() {
199209
return threadCount;
200210
}
201211

212+
/**
213+
* @return true if the last execute() call was cancelled/interrupted.
214+
*/
215+
public boolean isCancelled() {
216+
return cancelled;
217+
}
218+
202219
/**
203220
* @return maximum available threads (cores) detected at runtime.
204221
*/
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* VStar: a statistical analysis tool for variable star data.
3+
* Copyright (C) 2010 AAVSO (http://www.aavso.org/)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*/
10+
package org.aavso.tools.vstar.util.period.wwz;
11+
12+
import java.util.List;
13+
14+
import org.aavso.tools.vstar.data.ValidObservation;
15+
import org.aavso.tools.vstar.util.period.dcdft.DataTestBase;
16+
17+
/**
18+
* Verify cancellation semantics for threaded WWZ execution.
19+
*/
20+
public class WWZCancellationTest extends DataTestBase {
21+
22+
private static final double[][] LARGE_DATA = buildLargeData();
23+
24+
public WWZCancellationTest() {
25+
super("WWZ cancellation test", LARGE_DATA);
26+
}
27+
28+
public void testInterruptMarksCancelledAndClearsResults() throws Exception {
29+
final WeightedWaveletZTransform wwt = new WeightedWaveletZTransform(obs, 0.0005, 50.0);
30+
wwt.setThreadCount(WeightedWaveletZTransform.getRecommendedThreadCount());
31+
wwt.make_freqs_from_period_range(20.0, 300.0, 0.5);
32+
33+
Thread runThread = new Thread(new Runnable() {
34+
@Override
35+
public void run() {
36+
try {
37+
wwt.execute();
38+
} catch (Exception e) {
39+
// execute handles cancellation internally; no exception expected here
40+
}
41+
}
42+
});
43+
runThread.start();
44+
Thread.sleep(20);
45+
wwt.interrupt();
46+
runThread.join(10000);
47+
48+
assertTrue("WWZ execution thread should terminate after interrupt", !runThread.isAlive());
49+
assertTrue("WWZ should report cancelled state", wwt.isCancelled());
50+
assertEquals("Cancelled run should not expose partial stats", 0, wwt.getStats().size());
51+
assertEquals("Cancelled run should not expose partial maximal stats", 0, wwt.getMaximalStats().size());
52+
}
53+
54+
private static double[][] buildLargeData() {
55+
double[][] base = TUmi2420000To2425000Data.data;
56+
int repeats = 8;
57+
double[][] out = new double[base.length * repeats][2];
58+
double span = 5000.0;
59+
int p = 0;
60+
for (int r = 0; r < repeats; r++) {
61+
for (int i = 0; i < base.length; i++) {
62+
out[p][0] = base[i][0] + r * span;
63+
out[p][1] = base[i][1];
64+
p++;
65+
}
66+
}
67+
return out;
68+
}
69+
}
70+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* VStar: a statistical analysis tool for variable star data.
3+
* Copyright (C) 2010 AAVSO (http://www.aavso.org/)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*/
10+
package org.aavso.tools.vstar.util.period.wwz;
11+
12+
import java.util.List;
13+
14+
import org.aavso.tools.vstar.util.period.dcdft.DataTestBase;
15+
16+
/**
17+
* Benchmark WWZ runtime scaling across thread counts.
18+
*/
19+
public class WWZThreadScalingBenchmarkTest extends DataTestBase {
20+
21+
private static final double[][] LARGE_DATA = buildLargeData();
22+
23+
public WWZThreadScalingBenchmarkTest() {
24+
super("WWZ thread scaling benchmark", LARGE_DATA);
25+
}
26+
27+
public void testThreadScalingBenchmark() throws Exception {
28+
int[] threadCounts = new int[] { 1, 2, 4, 8, WeightedWaveletZTransform.getRecommendedThreadCount() };
29+
double minPeriod = 20.0;
30+
double maxPeriod = 300.0;
31+
double deltaPeriod = 0.5;
32+
double decay = 0.0005;
33+
double timeDivisions = 50.0;
34+
int iterations = 3;
35+
36+
double baselineMs = -1.0;
37+
System.out.println("WWZ thread scaling benchmark (synthetic >1000 obs, " + iterations + " runs each):");
38+
for (int tc : threadCounts) {
39+
long t0 = System.nanoTime();
40+
for (int i = 0; i < iterations; i++) {
41+
WeightedWaveletZTransform wwt = new WeightedWaveletZTransform(obs, decay, timeDivisions);
42+
wwt.setThreadCount(tc);
43+
wwt.make_freqs_from_period_range(minPeriod, maxPeriod, deltaPeriod);
44+
wwt.execute();
45+
List<WWZStatistic> stats = wwt.getStats();
46+
assertTrue(stats.size() > 0);
47+
}
48+
double totalMs = (System.nanoTime() - t0) / 1_000_000.0;
49+
if (baselineMs < 0.0) {
50+
baselineMs = totalMs;
51+
}
52+
double speedup = baselineMs / totalMs;
53+
System.out.println(" threads=" + tc + " total=" + String.format("%.2f", totalMs) + " ms speedup="
54+
+ String.format("%.2fx", speedup));
55+
}
56+
}
57+
58+
private static double[][] buildLargeData() {
59+
double[][] base = TUmi2420000To2425000Data.data;
60+
int repeats = 6;
61+
double[][] out = new double[base.length * repeats][2];
62+
double span = 5000.0;
63+
int p = 0;
64+
for (int r = 0; r < repeats; r++) {
65+
for (int i = 0; i < base.length; i++) {
66+
out[p][0] = base[i][0] + r * span;
67+
out[p][1] = base[i][1];
68+
p++;
69+
}
70+
}
71+
return out;
72+
}
73+
}
74+
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* VStar: a statistical analysis tool for variable star data.
3+
* Copyright (C) 2010 AAVSO (http://www.aavso.org/)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*/
10+
package org.aavso.tools.vstar.util.period.wwz;
11+
12+
import java.util.List;
13+
14+
import org.aavso.tools.vstar.util.period.dcdft.DataTestBase;
15+
16+
/**
17+
* Verify that multi-threaded WWZ execution matches single-threaded results.
18+
*/
19+
public class WWZThreadedConsistencyTest extends DataTestBase {
20+
21+
private static final double[][] LARGE_DATA = buildLargeData();
22+
23+
public WWZThreadedConsistencyTest() {
24+
super("WWZ threaded consistency test", LARGE_DATA);
25+
}
26+
27+
public void testThreadedMatchesSingleThread() throws Exception {
28+
double minPeriod = 20.0;
29+
double maxPeriod = 300.0;
30+
double deltaPeriod = 0.5;
31+
double decay = 0.0005;
32+
double timeDivisions = 50.0;
33+
34+
WeightedWaveletZTransform oneThread = new WeightedWaveletZTransform(obs, decay, timeDivisions);
35+
oneThread.setThreadCount(1);
36+
oneThread.make_freqs_from_period_range(minPeriod, maxPeriod, deltaPeriod);
37+
oneThread.execute();
38+
39+
WeightedWaveletZTransform manyThreads = new WeightedWaveletZTransform(obs, decay, timeDivisions);
40+
manyThreads.setThreadCount(WeightedWaveletZTransform.getRecommendedThreadCount());
41+
manyThreads.make_freqs_from_period_range(minPeriod, maxPeriod, deltaPeriod);
42+
manyThreads.execute();
43+
44+
List<WWZStatistic> a = oneThread.getStats();
45+
List<WWZStatistic> b = manyThreads.getStats();
46+
assertEquals(a.size(), b.size());
47+
for (int i = 0; i < a.size(); i++) {
48+
assertEquals(a.get(i).getTau(), b.get(i).getTau(), 1e-9);
49+
assertEquals(a.get(i).getFrequency(), b.get(i).getFrequency(), 1e-9);
50+
assertEquals(a.get(i).getWwz(), b.get(i).getWwz(), 1e-9);
51+
assertEquals(a.get(i).getSemiAmplitude(), b.get(i).getSemiAmplitude(), 1e-9);
52+
assertEquals(a.get(i).getMave(), b.get(i).getMave(), 1e-9);
53+
assertEquals(a.get(i).getNeff(), b.get(i).getNeff(), 1e-9);
54+
}
55+
56+
List<WWZStatistic> ma = oneThread.getMaximalStats();
57+
List<WWZStatistic> mb = manyThreads.getMaximalStats();
58+
assertEquals(ma.size(), mb.size());
59+
for (int i = 0; i < ma.size(); i++) {
60+
assertEquals(ma.get(i).getTau(), mb.get(i).getTau(), 1e-9);
61+
assertEquals(ma.get(i).getFrequency(), mb.get(i).getFrequency(), 1e-9);
62+
assertEquals(ma.get(i).getWwz(), mb.get(i).getWwz(), 1e-9);
63+
assertEquals(ma.get(i).getSemiAmplitude(), mb.get(i).getSemiAmplitude(), 1e-9);
64+
assertEquals(ma.get(i).getMave(), mb.get(i).getMave(), 1e-9);
65+
assertEquals(ma.get(i).getNeff(), mb.get(i).getNeff(), 1e-9);
66+
}
67+
}
68+
69+
private static double[][] buildLargeData() {
70+
double[][] base = TUmi2420000To2425000Data.data;
71+
int repeats = 6; // >1000 points to exercise threaded path heuristics
72+
double[][] out = new double[base.length * repeats][2];
73+
double span = 5000.0;
74+
int p = 0;
75+
for (int r = 0; r < repeats; r++) {
76+
for (int i = 0; i < base.length; i++) {
77+
out[p][0] = base[i][0] + r * span;
78+
out[p][1] = base[i][1];
79+
p++;
80+
}
81+
}
82+
return out;
83+
}
84+
}
85+

0 commit comments

Comments
 (0)