1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint;
7   
8   import java.net.URI;
9   import java.time.Instant;
10  import java.util.ArrayDeque;
11  import java.util.Collections;
12  import java.util.Deque;
13  import java.util.Map;
14  import java.util.concurrent.ConcurrentHashMap;
15  
16  import dev.metaschema.core.metapath.item.node.INodeItem;
17  import edu.umd.cs.findbugs.annotations.NonNull;
18  import edu.umd.cs.findbugs.annotations.Nullable;
19  
20  /**
21   * A {@link ValidationEventListener} implementation that collects timing
22   * measurements for all validation events.
23   * <p>
24   * Timing data is organized hierarchically:
25   * <ul>
26   * <li>Overall validation timing</li>
27   * <li>Per-phase timing (keyed by {@link ValidationPhase})</li>
28   * <li>Per-constraint timing (keyed by
29   * {@link IConstraint#getInternalIdentifier()})</li>
30   * <li>Per-let-statement timing (keyed by
31   * {@link ILet#getInternalIdentifier()})</li>
32   * </ul>
33   * <p>
34   * This class is thread-safe. Each thread maintains its own stack of start times
35   * to handle nested events correctly (e.g., a constraint evaluation that
36   * triggers let-statement evaluations).
37   *
38   * @see TimingRecord
39   * @see ValidationEventListener
40   */
41  public class TimingCollector implements ValidationEventListener {
42    @NonNull
43    private final ConcurrentHashMap<ValidationPhase, TimingRecord> phaseTimings = new ConcurrentHashMap<>();
44    @NonNull
45    private final ConcurrentHashMap<String, TimingRecord> constraintTimings = new ConcurrentHashMap<>();
46    @NonNull
47    private final ConcurrentHashMap<ILet, TimingRecord> letTimings = new ConcurrentHashMap<>();
48    @SuppressWarnings("PMD.AvoidUsingVolatile") // Required for thread-safe publication
49    @Nullable
50    private volatile TimingRecord validationTiming;
51  
52    /**
53     * Thread-local stacks of nano-time start values, used to support nested
54     * before/after event pairs.
55     */
56    @NonNull
57    private final ThreadLocal<Deque<Long>> startTimeStack = ThreadLocal.withInitial(ArrayDeque::new);
58  
59    /**
60     * Construct a new, empty timing collector.
61     */
62    public TimingCollector() {
63      // nothing to initialize beyond field defaults
64    }
65  
66    /**
67     * Push a start time onto the current thread's stack.
68     */
69    private void pushStartTime() {
70      startTimeStack.get().push(System.nanoTime());
71    }
72  
73    /**
74     * Pop and return the most recent start time from the current thread's stack.
75     *
76     * @return the elapsed duration in nanoseconds since the corresponding push
77     */
78    private long popElapsedNs() {
79      Long startNs = startTimeStack.get().poll();
80      if (startNs == null) {
81        return 0L;
82      }
83      return System.nanoTime() - startNs;
84    }
85  
86    @Override
87    public void beforeValidation(@NonNull URI document) {
88      TimingRecord record = validationTiming;
89      if (record == null) {
90        record = new TimingRecord();
91        validationTiming = record;
92      }
93      record.recordStart(Instant.now());
94      pushStartTime();
95    }
96  
97    @Override
98    public void afterValidation(@NonNull URI document) {
99      long elapsed = popElapsedNs();
100     TimingRecord record = validationTiming;
101     if (record != null) {
102       record.recordEnd(elapsed, Instant.now());
103     }
104   }
105 
106   @Override
107   public void beforePhase(@NonNull ValidationPhase phase) {
108     phaseTimings.computeIfAbsent(phase, k -> new TimingRecord())
109         .recordStart(Instant.now());
110     pushStartTime();
111   }
112 
113   @Override
114   public void afterPhase(@NonNull ValidationPhase phase) {
115     long elapsed = popElapsedNs();
116     TimingRecord record = phaseTimings.get(phase);
117     if (record != null) {
118       record.recordEnd(elapsed, Instant.now());
119     }
120   }
121 
122   @Override
123   public void beforeConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) {
124     String id = constraint.getInternalIdentifier();
125     constraintTimings.computeIfAbsent(id, k -> new TimingRecord())
126         .recordStart(Instant.now());
127     pushStartTime();
128   }
129 
130   @Override
131   public void afterConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) {
132     long elapsed = popElapsedNs();
133     String id = constraint.getInternalIdentifier();
134     TimingRecord record = constraintTimings.get(id);
135     if (record != null) {
136       record.recordEnd(elapsed, Instant.now());
137     }
138   }
139 
140   @Override
141   public void beforeLetEvaluation(@NonNull ILet let) {
142     letTimings.computeIfAbsent(let, k -> new TimingRecord())
143         .recordStart(Instant.now());
144     pushStartTime();
145   }
146 
147   @Override
148   public void afterLetEvaluation(@NonNull ILet let) {
149     long elapsed = popElapsedNs();
150     TimingRecord record = letTimings.get(let);
151     if (record != null) {
152       record.recordEnd(elapsed, Instant.now());
153     }
154   }
155 
156   /**
157    * Get the timing record for a specific validation phase.
158    *
159    * @param phase
160    *          the phase to look up
161    * @return the timing record, or {@code null} if the phase was not recorded
162    */
163   @Nullable
164   public TimingRecord getPhaseTiming(@NonNull ValidationPhase phase) {
165     return phaseTimings.get(phase);
166   }
167 
168   /**
169    * Get all phase timing records.
170    *
171    * @return an unmodifiable map of phase to timing record
172    */
173   @NonNull
174   public Map<ValidationPhase, TimingRecord> getPhaseTimings() {
175     return Collections.unmodifiableMap(phaseTimings);
176   }
177 
178   /**
179    * Get the timing record for a specific constraint by its internal identifier.
180    *
181    * @param constraintId
182    *          the constraint's internal identifier
183    * @return the timing record, or {@code null} if the constraint was not recorded
184    */
185   @Nullable
186   public TimingRecord getConstraintTiming(@NonNull String constraintId) {
187     return constraintTimings.get(constraintId);
188   }
189 
190   /**
191    * Get all constraint timing records.
192    *
193    * @return an unmodifiable map of constraint identifier to timing record
194    */
195   @NonNull
196   public Map<String, TimingRecord> getConstraintTimings() {
197     return Collections.unmodifiableMap(constraintTimings);
198   }
199 
200   /**
201    * Get the timing record for a specific let-statement.
202    *
203    * @param let
204    *          the let-statement to look up
205    * @return the timing record, or {@code null} if the let was not recorded
206    */
207   @Nullable
208   public TimingRecord getLetTiming(@NonNull ILet let) {
209     return letTimings.get(let);
210   }
211 
212   /**
213    * Get all let-statement timing records.
214    *
215    * @return an unmodifiable map of let-statement to timing record
216    */
217   @NonNull
218   public Map<ILet, TimingRecord> getLetTimings() {
219     return Collections.unmodifiableMap(letTimings);
220   }
221 
222   /**
223    * Get the overall validation timing record.
224    *
225    * @return the validation timing record, or {@code null} if validation was not
226    *         recorded
227    */
228   @Nullable
229   public TimingRecord getValidationTiming() {
230     return validationTiming;
231   }
232 }