Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2014 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | package com.google.devtools.build.lib.analysis; |
| 15 | |
| 16 | import com.google.common.base.Joiner; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 17 | import com.google.common.collect.ImmutableList; |
| 18 | import com.google.common.collect.ImmutableSet; |
| 19 | import com.google.common.collect.Lists; |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 20 | import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 21 | import com.google.devtools.build.lib.actions.Artifact; |
| 22 | import com.google.devtools.build.lib.actions.ArtifactFactory; |
| 23 | import com.google.devtools.build.lib.actions.ArtifactOwner; |
| 24 | import com.google.devtools.build.lib.actions.MiddlemanFactory; |
| 25 | import com.google.devtools.build.lib.actions.Root; |
| 26 | import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoCollection; |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 27 | import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 28 | import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey; |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 29 | import com.google.devtools.build.lib.analysis.config.BuildConfiguration; |
Klaus Aehlig | 777b30d | 2017-02-24 16:30:15 +0000 | [diff] [blame] | 30 | import com.google.devtools.build.lib.events.ExtendedEventHandler; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 31 | import com.google.devtools.build.lib.events.StoredEventHandler; |
| 32 | import com.google.devtools.build.lib.packages.Target; |
| 33 | import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue; |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 34 | import com.google.devtools.build.lib.skyframe.PrecomputedValue; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 35 | import com.google.devtools.build.lib.skyframe.WorkspaceStatusValue; |
brandjon | b712f33 | 2017-04-29 16:03:32 +0200 | [diff] [blame] | 36 | import com.google.devtools.build.lib.syntax.SkylarkSemanticsOptions; |
Mark Schaller | 6df8179 | 2015-12-10 18:47:47 +0000 | [diff] [blame] | 37 | import com.google.devtools.build.lib.util.Preconditions; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 38 | import com.google.devtools.build.lib.vfs.PathFragment; |
| 39 | import com.google.devtools.build.skyframe.SkyFunction; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 40 | import java.io.PrintWriter; |
| 41 | import java.io.StringWriter; |
| 42 | import java.util.ArrayList; |
| 43 | import java.util.Collection; |
| 44 | import java.util.Collections; |
| 45 | import java.util.HashMap; |
| 46 | import java.util.HashSet; |
| 47 | import java.util.List; |
| 48 | import java.util.Map; |
| 49 | import java.util.Set; |
| 50 | import java.util.TreeMap; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 51 | import javax.annotation.Nullable; |
| 52 | |
| 53 | /** |
| 54 | * The implementation of AnalysisEnvironment used for analysis. It tracks metadata for each |
| 55 | * configured target, such as the errors and warnings emitted by that target. It is intended that |
| 56 | * a separate instance is used for each configured target, so that these don't mix up. |
| 57 | */ |
| 58 | public class CachingAnalysisEnvironment implements AnalysisEnvironment { |
| 59 | private final ArtifactFactory artifactFactory; |
| 60 | |
| 61 | private final ArtifactOwner owner; |
| 62 | /** |
| 63 | * If this is the system analysis environment, then errors and warnings are directly reported |
| 64 | * to the global reporter, rather than stored, i.e., we don't track here whether there are any |
| 65 | * errors. |
| 66 | */ |
| 67 | private final boolean isSystemEnv; |
| 68 | private final boolean extendedSanityChecks; |
| 69 | |
| 70 | /** |
| 71 | * If false, no actions will be registered, they'll all be just dropped. |
| 72 | * |
| 73 | * <p>Usually, an analysis environment should register all actions. However, in some scenarios we |
| 74 | * analyze some targets twice, but the first one only serves the purpose of collecting information |
| 75 | * for the second analysis. In this case we don't register actions created by the first pass in |
| 76 | * order to avoid action conflicts. |
| 77 | */ |
| 78 | private final boolean allowRegisteringActions; |
| 79 | |
| 80 | private boolean enabled = true; |
| 81 | private MiddlemanFactory middlemanFactory; |
Klaus Aehlig | 777b30d | 2017-02-24 16:30:15 +0000 | [diff] [blame] | 82 | private ExtendedEventHandler errorEventListener; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 83 | private SkyFunction.Environment skyframeEnv; |
| 84 | private Map<Artifact, String> artifacts; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 85 | |
| 86 | /** |
| 87 | * The list of actions registered by the configured target this analysis environment is |
| 88 | * responsible for. May get cleared out at the end of the analysis of said target. |
| 89 | */ |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 90 | final List<ActionAnalysisMetadata> actions = new ArrayList<>(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 91 | |
Klaus Aehlig | 777b30d | 2017-02-24 16:30:15 +0000 | [diff] [blame] | 92 | public CachingAnalysisEnvironment( |
| 93 | ArtifactFactory artifactFactory, |
| 94 | ArtifactOwner owner, |
| 95 | boolean isSystemEnv, |
| 96 | boolean extendedSanityChecks, |
| 97 | ExtendedEventHandler errorEventListener, |
| 98 | SkyFunction.Environment env, |
Lukacs Berki | 76cb02e | 2017-02-17 14:06:11 +0000 | [diff] [blame] | 99 | boolean allowRegisteringActions) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 100 | this.artifactFactory = artifactFactory; |
| 101 | this.owner = Preconditions.checkNotNull(owner); |
| 102 | this.isSystemEnv = isSystemEnv; |
| 103 | this.extendedSanityChecks = extendedSanityChecks; |
| 104 | this.errorEventListener = errorEventListener; |
| 105 | this.skyframeEnv = env; |
| 106 | this.allowRegisteringActions = allowRegisteringActions; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 107 | middlemanFactory = new MiddlemanFactory(artifactFactory, this); |
| 108 | artifacts = new HashMap<>(); |
| 109 | } |
| 110 | |
| 111 | public void disable(Target target) { |
| 112 | if (!hasErrors() && allowRegisteringActions) { |
| 113 | verifyGeneratedArtifactHaveActions(target); |
| 114 | } |
| 115 | artifacts = null; |
| 116 | middlemanFactory = null; |
| 117 | enabled = false; |
| 118 | errorEventListener = null; |
| 119 | skyframeEnv = null; |
| 120 | } |
| 121 | |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 122 | private static StringBuilder shortDescription(ActionAnalysisMetadata action) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 123 | if (action == null) { |
| 124 | return new StringBuilder("null Action"); |
| 125 | } |
| 126 | return new StringBuilder() |
| 127 | .append(action.getClass().getName()) |
| 128 | .append(' ') |
| 129 | .append(action.getMnemonic()); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Sanity checks that all generated artifacts have a generating action. |
| 134 | * @param target for error reporting |
| 135 | */ |
| 136 | public void verifyGeneratedArtifactHaveActions(Target target) { |
| 137 | Collection<String> orphanArtifacts = getOrphanArtifactMap().values(); |
| 138 | List<String> checkedActions = null; |
| 139 | if (!orphanArtifacts.isEmpty()) { |
| 140 | checkedActions = Lists.newArrayListWithCapacity(actions.size()); |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 141 | for (ActionAnalysisMetadata action : actions) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 142 | StringBuilder sb = shortDescription(action); |
| 143 | for (Artifact o : action.getOutputs()) { |
| 144 | sb.append("\n "); |
| 145 | sb.append(o.getExecPathString()); |
| 146 | } |
| 147 | checkedActions.add(sb.toString()); |
| 148 | } |
| 149 | throw new IllegalStateException( |
| 150 | String.format( |
Alex Humesky | 14f79d5 | 2015-07-08 18:39:15 +0000 | [diff] [blame] | 151 | "%s %s : These artifacts do not have a generating action:\n%s\n" |
| 152 | + "These actions were checked:\n%s\n", |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 153 | target.getTargetKind(), target.getLabel(), |
| 154 | Joiner.on('\n').join(orphanArtifacts), Joiner.on('\n').join(checkedActions))); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | @Override |
| 159 | public ImmutableSet<Artifact> getOrphanArtifacts() { |
Ulf Adams | d68626a | 2016-02-01 14:58:45 +0000 | [diff] [blame] | 160 | if (!allowRegisteringActions) { |
| 161 | return ImmutableSet.<Artifact>of(); |
| 162 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 163 | return ImmutableSet.copyOf(getOrphanArtifactMap().keySet()); |
| 164 | } |
| 165 | |
| 166 | private Map<Artifact, String> getOrphanArtifactMap() { |
| 167 | // Construct this set to avoid poor performance under large --runs_per_test. |
| 168 | Set<Artifact> artifactsWithActions = new HashSet<>(); |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 169 | for (ActionAnalysisMetadata action : actions) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 170 | // Don't bother checking that every Artifact only appears once; that test is performed |
| 171 | // elsewhere (see #testNonUniqueOutputs in ActionListenerIntegrationTest). |
| 172 | artifactsWithActions.addAll(action.getOutputs()); |
| 173 | } |
| 174 | // The order of the artifacts.entrySet iteration is unspecified - we use a TreeMap here to |
| 175 | // guarantee that the return value of this method is deterministic. |
Lukacs Berki | 8ae34f1 | 2015-04-10 14:54:19 +0000 | [diff] [blame] | 176 | Map<Artifact, String> orphanArtifacts = new TreeMap<>(Artifact.EXEC_PATH_COMPARATOR); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 177 | for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) { |
| 178 | Artifact a = entry.getKey(); |
| 179 | if (!a.isSourceArtifact() && !artifactsWithActions.contains(a)) { |
| 180 | orphanArtifacts.put(a, String.format("%s\n%s", |
| 181 | a.getExecPathString(), // uncovered artifact |
| 182 | entry.getValue())); // origin of creation |
| 183 | } |
| 184 | } |
| 185 | return orphanArtifacts; |
| 186 | } |
| 187 | |
| 188 | @Override |
Klaus Aehlig | 777b30d | 2017-02-24 16:30:15 +0000 | [diff] [blame] | 189 | public ExtendedEventHandler getEventHandler() { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 190 | return errorEventListener; |
| 191 | } |
| 192 | |
| 193 | @Override |
| 194 | public boolean hasErrors() { |
| 195 | // The system analysis environment never has errors. |
| 196 | if (isSystemEnv) { |
| 197 | return false; |
| 198 | } |
| 199 | Preconditions.checkState(enabled); |
| 200 | return ((StoredEventHandler) errorEventListener).hasErrors(); |
| 201 | } |
| 202 | |
| 203 | @Override |
| 204 | public MiddlemanFactory getMiddlemanFactory() { |
| 205 | Preconditions.checkState(enabled); |
| 206 | return middlemanFactory; |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Keeps track of artifacts. We check that all of them have an owner when the environment is |
| 211 | * sealed (disable()). For performance reasons we only track the originating stacktrace when |
| 212 | * running with --experimental_extended_sanity_checks. |
| 213 | */ |
| 214 | private Artifact trackArtifactAndOrigin(Artifact a, @Nullable Throwable e) { |
| 215 | if ((e != null) && !artifacts.containsKey(a)) { |
| 216 | StringWriter sw = new StringWriter(); |
| 217 | e.printStackTrace(new PrintWriter(sw)); |
| 218 | artifacts.put(a, sw.toString()); |
| 219 | } else { |
| 220 | artifacts.put(a, "No origin, run with --experimental_extended_sanity_checks"); |
| 221 | } |
| 222 | return a; |
| 223 | } |
| 224 | |
| 225 | @Override |
| 226 | public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) { |
| 227 | Preconditions.checkState(enabled); |
| 228 | return trackArtifactAndOrigin( |
| 229 | artifactFactory.getDerivedArtifact(rootRelativePath, root, getOwner()), |
| 230 | extendedSanityChecks ? new Throwable() : null); |
| 231 | } |
| 232 | |
| 233 | @Override |
Michael Thvedt | e3b1cb7 | 2016-02-08 23:32:27 +0000 | [diff] [blame] | 234 | public Artifact getTreeArtifact(PathFragment rootRelativePath, Root root) { |
| 235 | Preconditions.checkState(enabled); |
| 236 | return trackArtifactAndOrigin( |
| 237 | artifactFactory.getTreeArtifact(rootRelativePath, root, getOwner()), |
| 238 | extendedSanityChecks ? new Throwable() : null); |
| 239 | } |
| 240 | |
| 241 | @Override |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 242 | public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root) { |
| 243 | Preconditions.checkState(enabled); |
| 244 | return trackArtifactAndOrigin( |
| 245 | artifactFactory.getFilesetArtifact(rootRelativePath, root, getOwner()), |
| 246 | extendedSanityChecks ? new Throwable() : null); |
| 247 | } |
| 248 | |
| 249 | @Override |
| 250 | public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root) { |
| 251 | return artifactFactory.getConstantMetadataArtifact(rootRelativePath, root, getOwner()); |
| 252 | } |
| 253 | |
| 254 | @Override |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 255 | public void registerAction(ActionAnalysisMetadata... actions) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 256 | Preconditions.checkState(enabled); |
| 257 | if (allowRegisteringActions) { |
Ulf Adams | 07dba94 | 2015-03-05 14:47:37 +0000 | [diff] [blame] | 258 | Collections.addAll(this.actions, actions); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 259 | } |
| 260 | } |
| 261 | |
| 262 | @Override |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 263 | public ActionAnalysisMetadata getLocalGeneratingAction(Artifact artifact) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 264 | Preconditions.checkState(allowRegisteringActions); |
Rumou Duan | 33bab46 | 2016-04-25 17:55:12 +0000 | [diff] [blame] | 265 | for (ActionAnalysisMetadata action : actions) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 266 | if (action.getOutputs().contains(artifact)) { |
| 267 | return action; |
| 268 | } |
| 269 | } |
| 270 | return null; |
| 271 | } |
| 272 | |
| 273 | @Override |
janakr | 93e3eea | 2017-03-30 22:09:37 +0000 | [diff] [blame] | 274 | public List<ActionAnalysisMetadata> getRegisteredActions() { |
| 275 | return Collections.unmodifiableList(actions); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 276 | } |
| 277 | |
| 278 | @Override |
| 279 | public SkyFunction.Environment getSkyframeEnv() { |
| 280 | return skyframeEnv; |
| 281 | } |
| 282 | |
| 283 | @Override |
brandjon | b712f33 | 2017-04-29 16:03:32 +0200 | [diff] [blame] | 284 | public SkylarkSemanticsOptions getSkylarkSemantics() throws InterruptedException { |
| 285 | return PrecomputedValue.SKYLARK_SEMANTICS.get(skyframeEnv); |
| 286 | } |
| 287 | |
| 288 | @Override |
Janak Ramakrishnan | 3c0adb2 | 2016-08-15 21:54:55 +0000 | [diff] [blame] | 289 | public Artifact getStableWorkspaceStatusArtifact() throws InterruptedException { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 290 | return ((WorkspaceStatusValue) skyframeEnv.getValue(WorkspaceStatusValue.SKY_KEY)) |
| 291 | .getStableArtifact(); |
| 292 | } |
| 293 | |
| 294 | @Override |
Janak Ramakrishnan | 3c0adb2 | 2016-08-15 21:54:55 +0000 | [diff] [blame] | 295 | public Artifact getVolatileWorkspaceStatusArtifact() throws InterruptedException { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 296 | return ((WorkspaceStatusValue) skyframeEnv.getValue(WorkspaceStatusValue.SKY_KEY)) |
| 297 | .getVolatileArtifact(); |
| 298 | } |
| 299 | |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 300 | // See SkyframeBuildView#getWorkspaceStatusValues for the code that this method is attempting to |
| 301 | // verify. |
Janak Ramakrishnan | 3c0adb2 | 2016-08-15 21:54:55 +0000 | [diff] [blame] | 302 | private NullPointerException collectDebugInfoAndCrash(BuildInfoKey key, BuildConfiguration config) |
| 303 | throws InterruptedException { |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 304 | String debugInfo = key + " " + config; |
| 305 | Preconditions.checkState(skyframeEnv.valuesMissing(), debugInfo); |
| 306 | Map<BuildInfoKey, BuildInfoFactory> buildInfoFactories = Preconditions.checkNotNull( |
| 307 | PrecomputedValue.BUILD_INFO_FACTORIES.get(skyframeEnv), debugInfo); |
| 308 | BuildInfoFactory buildInfoFactory = |
| 309 | Preconditions.checkNotNull(buildInfoFactories.get(key), debugInfo); |
| 310 | Preconditions.checkState(buildInfoFactory.isEnabled(config), debugInfo); |
| 311 | throw new NullPointerException("BuildInfoCollectionValue shouldn't have been null"); |
| 312 | } |
| 313 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 314 | @Override |
Googler | 38ad0bf | 2016-07-01 05:00:14 +0000 | [diff] [blame] | 315 | public ImmutableList<Artifact> getBuildInfo( |
Janak Ramakrishnan | 3c0adb2 | 2016-08-15 21:54:55 +0000 | [diff] [blame] | 316 | RuleContext ruleContext, BuildInfoKey key, BuildConfiguration config) |
| 317 | throws InterruptedException { |
Googler | 38ad0bf | 2016-07-01 05:00:14 +0000 | [diff] [blame] | 318 | boolean stamp = AnalysisUtils.isStampingEnabled(ruleContext, config); |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 319 | BuildInfoCollectionValue collectionValue = |
| 320 | (BuildInfoCollectionValue) skyframeEnv.getValue(BuildInfoCollectionValue.key( |
Googler | 38ad0bf | 2016-07-01 05:00:14 +0000 | [diff] [blame] | 321 | new BuildInfoCollectionValue.BuildInfoKeyAndConfig(key, config))); |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 322 | if (collectionValue == null) { |
Googler | 38ad0bf | 2016-07-01 05:00:14 +0000 | [diff] [blame] | 323 | throw collectDebugInfoAndCrash(key, config); |
Janak Ramakrishnan | f27f438 | 2015-06-04 19:50:15 +0000 | [diff] [blame] | 324 | } |
| 325 | BuildInfoCollection collection = collectionValue.getCollection(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 326 | return stamp ? collection.getStampedBuildInfo() : collection.getRedactedBuildInfo(); |
| 327 | } |
| 328 | |
| 329 | @Override |
| 330 | public ArtifactOwner getOwner() { |
| 331 | return owner; |
| 332 | } |
| 333 | } |