Home | History | Annotate | Download | only in db
      1 /*******************************************************************************
      2  * Copyright (c) 2000, 2009 IBM Corporation and others.
      3  * All rights reserved. This program and the accompanying materials
      4  * are made available under the terms of the Eclipse Public License v1.0
      5  * which accompanies this distribution, and is available at
      6  * http://www.eclipse.org/legal/epl-v10.html
      7  *
      8  * Contributors:
      9  *     IBM Corporation - initial API and implementation
     10  *******************************************************************************/
     11 package org.eclipse.test.internal.performance.results.db;
     12 
     13 import java.io.DataInputStream;
     14 import java.io.DataOutputStream;
     15 import java.io.IOException;
     16 import java.util.ArrayList;
     17 import java.util.List;
     18 import org.eclipse.test.internal.performance.InternalDimensions;
     19 import org.eclipse.test.internal.performance.results.utils.Util;
     20 
     21 /**
     22  * Class to handle results for an Eclipse performance test box
     23  * (called a <i>configuration</i>).
     24  *
     25  * It gives access to results for each build on which this configuration has been run.
     26  *
     27  * @see BuildResults
     28  */
     29 public class ConfigResults extends AbstractResults {
     30 	BuildResults baseline, current;
     31 	boolean baselined = false, valid = false;
     32 	double delta, error;
     33 
     34 public ConfigResults(AbstractResults parent, int id) {
     35 	super(parent, id);
     36 	this.name = parent.getPerformance().sortedConfigNames[id];
     37 	this.printStream = parent.printStream;
     38 }
     39 
     40 /*
     41  * Complete results with additional database information.
     42  */
     43 void completeResults(String[] builds) {
     44 	/*if (this.baseline == null || this.current == null) */initialize();
     45 	ScenarioResults scenarioResults = (ScenarioResults) this.parent;
     46 	DB_Results.queryScenarioSummaries(scenarioResults, this.name, builds);
     47 }
     48 
     49 /**
     50  * Returns the baseline build name used to compare results with.
     51  *
     52  * @return The name of the baseline build
     53  * @see #getBaselineBuildResults()
     54  */
     55 public String getBaselineBuildName() {
     56 	if (this.baseline == null) {
     57 	    initialize();
     58     }
     59 	return this.baseline.getName();
     60 }
     61 
     62 /**
     63  * Returns the most recent baseline build results.
     64  *
     65  * @return The {@link BuildResults baseline build results}.
     66  * @see BuildResults
     67  */
     68 public BuildResults getBaselineBuildResults() {
     69 	if (this.baseline == null) {
     70 	    initialize();
     71     }
     72 	return this.baseline;
     73 }
     74 
     75 /**
     76  * Return the baseline build results run just before the given build name.
     77  *
     78  * @param buildName The build name
     79  * @return The {@link BuildResults baseline results} preceding the given build name
     80  * 	or <code>null</code> if none was found.
     81  */
     82 public BuildResults getBaselineBuildResults(String buildName) {
     83 	if (this.baseline == null) {
     84 	    initialize();
     85     }
     86 	int size = this.children.size();
     87 	String buildDate = Util.getBuildDate(buildName);
     88 	for (int i=size-1; i>=0; i--) {
     89 		BuildResults buildResults = (BuildResults) this.children.get(i);
     90 		if (buildResults.isBaseline() && buildResults.getDate().compareTo(buildDate) < 0) {
     91 			return buildResults;
     92 		}
     93 	}
     94 	return this.baseline;
     95 
     96 }
     97 
     98 /**
     99  * Returns the most recent baseline build result value.
    100  *
    101  * @return The value of the most recent baseline build results.
    102  */
    103 public double getBaselineBuildValue() {
    104 	if (this.baseline == null) {
    105 	    initialize();
    106     }
    107 	return this.baseline.getValue();
    108 }
    109 
    110 /**
    111  * Returns the configuration description (currently the box name).
    112  *
    113  * @return The configuration description (currently the box name).
    114  */
    115 public String getDescription() {
    116 	return getPerformance().sortedConfigDescriptions[this.id];
    117 }
    118 
    119 /**
    120  * Return the results for the given build name.
    121  *
    122  * @param buildName The build name
    123  * @return The {@link BuildResults results} for the given build name
    124  * 	or <code>null</code> if none was found.
    125  */
    126 public BuildResults getBuildResults(String buildName) {
    127 	return (BuildResults) getResults(buildName);
    128 }
    129 
    130 /**
    131  * Returns the build results matching a given pattern.
    132  *
    133  * @param buildPattern The pattern of searched builds
    134  * @return The list of the builds which names match the given pattern.
    135  * 	The list is ordered by build results date.
    136  */
    137 public List getBuilds(String buildPattern) {
    138 	List builds = new ArrayList();
    139 	int size = size();
    140 	for (int i=0; i<size; i++) {
    141 		BuildResults buildResults = (BuildResults) this.children.get(i);
    142 		if (buildPattern == null || buildResults.match(buildPattern)) {
    143 			builds.add(buildResults);
    144 		}
    145 	}
    146 	return builds;
    147 }
    148 
    149 /**
    150  * Returns the build results before a given build name.
    151  *
    152  * @param buildName Name of the last build (included)
    153  * @return The list of the builds which precedes the given build name.
    154  */
    155 public List getBuildsBefore(String buildName) {
    156 	String buildDate = Util.getBuildDate(buildName);
    157 	List builds = new ArrayList();
    158 	int size = size();
    159 	for (int i=0; i<size; i++) {
    160 		BuildResults buildResults = (BuildResults) this.children.get(i);
    161 		if (buildName == null || buildResults.getDate().compareTo(buildDate) <= 0) {
    162 			builds.add(buildResults);
    163 		}
    164 	}
    165 	return builds;
    166 }
    167 
    168 /**
    169  * Returns a list of build results which names starts with one of the given prefixes.
    170  *
    171  * @param prefixes List of expected prefixes
    172  * @return A list of builds which names start with one of the given patterns.
    173  */
    174 public List getBuildsMatchingPrefixes(List prefixes) {
    175 	List builds = new ArrayList();
    176 	int size = size();
    177 	int length = prefixes.size();
    178 	for (int i=0; i<size; i++) {
    179 		AbstractResults buildResults = (AbstractResults) this.children.get(i);
    180 		String buildName = buildResults.getName();
    181 		for (int j=0; j<length; j++) {
    182 			if (buildName.startsWith((String)prefixes.get(j))) {
    183 				builds.add(buildResults);
    184 			}
    185 		}
    186 	}
    187 	return builds;
    188 }
    189 
    190 /**
    191  * Get all results numbers for the max last builds.
    192  *
    193  * @param max The number of last builds to get numbers.
    194  * @return An 2 dimensions array of doubles. At the first level of the array each slot
    195  * 		represents one build. That means that the dimension of the array matches
    196  * 		the given numbers as soon as there are enough builds in the database.
    197  * <p>
    198  * 		The slots of the second level are the numbers values:
    199  * 	<ul>
    200  * 		<li>{@link #BUILD_VALUE_INDEX}: the build value in milliseconds</li>
    201  * 		<li>{@link #BASELINE_VALUE_INDEX}: the baseline value in milliseconds</li>
    202  * 		<li>{@link #DELTA_VALUE_INDEX}: the difference between the build value and its more recent baseline</li>
    203  * 		<li>{@link #DELTA_ERROR_INDEX}: the error made while computing the difference</li>
    204  * 		<li>{@link #BUILD_ERROR_INDEX}: the error made while measuring the build value</li>
    205  * 		<li>{@link #BASELINE_ERROR_INDEX}: the error made while measuring the baseline value</li>
    206  * 	</ul>
    207 */
    208 public double[][] getLastNumbers(int max) {
    209 
    210 	// Return null if no previous builds are expected
    211 	if (max <= 0) return null;
    212 
    213 	// Add numbers for each previous build
    214 	int size = size();
    215 	double[][] numbers = new double[Math.min(max, size)][];
    216 	int n = 0;
    217 	for (int i=size-1; i>=0 && n<max; i--) {
    218 		BuildResults buildResults = (BuildResults) this.children.get(i);
    219 		if (!buildResults.isBaseline()) {
    220 			numbers[n] = getNumbers(buildResults, getBaselineBuildResults(buildResults.getName()));
    221 			n++;
    222 		}
    223 	}
    224 
    225 	// Return the numbers
    226 	return numbers;
    227 }
    228 
    229 /**
    230  * Returns interesting numbers for the current configuration.
    231  *
    232  * @return Values in an array of double:
    233  * 	<ul>
    234  * 		<li>{@link AbstractResults#BUILD_VALUE_INDEX}: the build value in milliseconds</li>
    235  * 		<li>{@link AbstractResults#BASELINE_VALUE_INDEX}: the baseline value in milliseconds</li>
    236  * 		<li>{@link AbstractResults#DELTA_VALUE_INDEX}: the difference between the build value and its more recent baseline</li>
    237  * 		<li>{@link AbstractResults#DELTA_ERROR_INDEX}: the error made while computing the difference</li>
    238  * 		<li>{@link AbstractResults#BUILD_ERROR_INDEX}: the error made while measuring the build value</li>
    239  * 		<li>{@link AbstractResults#BASELINE_ERROR_INDEX}: the error made while measuring the baseline value</li>
    240  * 	</ul>
    241  */
    242 double[] getNumbers(BuildResults buildResults, BuildResults baselineResults) {
    243 	if (baselineResults == null) {
    244 		return null;
    245 	}
    246 	double[] values = new double[NUMBERS_LENGTH];
    247 	for (int i=0 ;i<NUMBERS_LENGTH; i++) {
    248 		values[i] = Double.NaN;
    249 	}
    250 	double buildValue = buildResults.getValue();
    251 	values[BUILD_VALUE_INDEX] = buildValue;
    252 	double baselineValue = baselineResults.getValue();
    253 	values[BASELINE_VALUE_INDEX] = baselineValue;
    254 	double buildDelta = (baselineValue - buildValue) / baselineValue;
    255 	values[DELTA_VALUE_INDEX] = buildDelta;
    256 	if (Double.isNaN(buildDelta)) {
    257 		return values;
    258 	}
    259 	long baselineCount = baselineResults.getCount();
    260 	long currentCount = buildResults.getCount();
    261 	if (baselineCount > 1 && currentCount > 1) {
    262 		double baselineError = baselineResults.getError();
    263 		double currentError = buildResults.getError();
    264 		values[BASELINE_ERROR_INDEX] = baselineError;
    265 		values[BUILD_ERROR_INDEX] = currentError;
    266 		values[DELTA_ERROR_INDEX] = Double.isNaN(baselineError)
    267 				? currentError / baselineValue
    268 				: Math.sqrt(baselineError*baselineError + currentError*currentError) / baselineValue;
    269 	}
    270 	return values;
    271 }
    272 
    273 /**
    274  * Return the deviation value and its associated standard error for the default dimension
    275  * (currently {@link InternalDimensions#ELAPSED_PROCESS}).
    276  *
    277  * @return an array of double. First number is the deviation itself and
    278  * 	the second is the standard error.
    279  */
    280 public double[] getCurrentBuildDeltaInfo() {
    281 	if (this.baseline == null || this.current == null) {
    282 		initialize();
    283 	}
    284 	return new double[] { this.delta, this.error };
    285 }
    286 
    287 /**
    288  * Returns the error of the current build results
    289  *
    290  * @return the error made during the current build measure
    291  */
    292 public double getCurrentBuildError() {
    293 	if (this.current == null) {
    294 	    initialize();
    295     }
    296 	return this.current.getError();
    297 }
    298 
    299 /**
    300  * Returns the current build name.
    301  *
    302  * @return The name of the current build
    303  * @see #getCurrentBuildResults()
    304  */
    305 public String getCurrentBuildName() {
    306 	if (this.current == null) {
    307 	    initialize();
    308     }
    309 	return this.current.getName();
    310 }
    311 
    312 /**
    313  * Returns the current build results.
    314  * <p>
    315  * This build is currently the last integration or nightly
    316  * build which has performance results in the database.
    317  * It may differ from the {@link PerformanceResults#getName()}.
    318  *
    319  * @return The current build results.
    320  * @see BuildResults
    321  */
    322 public BuildResults getCurrentBuildResults() {
    323 	if (this.current == null) {
    324 	    initialize();
    325     }
    326 	return this.current;
    327 }
    328 
    329 /**
    330  * Returns the current build result value.
    331  *
    332  * @return The value of the current build results.
    333  */
    334 public double getCurrentBuildValue() {
    335 	if (this.current == null) {
    336 	    initialize();
    337     }
    338 	return this.current.getValue();
    339 }
    340 
    341 /**
    342  * Returns the delta between current and baseline builds results.
    343  *
    344  * @return the delta
    345  */
    346 public double getDelta() {
    347 	if (this.baseline == null || this.current == null) {
    348 		initialize();
    349 	}
    350 	return this.delta;
    351 }
    352 
    353 /**
    354  * Returns the standard error of the delta between current and baseline builds results.
    355  *
    356  * @return the delta
    357  * @see #getDelta()
    358  */
    359 public double getError() {
    360 	if (this.baseline == null || this.current == null) {
    361 		initialize();
    362 	}
    363 	return this.error;
    364 }
    365 
    366 /**
    367  * Return the name of the machine associated with the current config.
    368  *
    369  * @return The name of the machine.
    370  */
    371 public String getLabel() {
    372 	return this.parent.getPerformance().sortedConfigDescriptions[this.id];
    373 }
    374 
    375 /**
    376  * Get all dimension builds default dimension statistics for all builds.
    377  *
    378  * @return An array of double built as follows:
    379  * <ul>
    380  * <li>0:	numbers of values</li>
    381  * <li>1:	mean of values</li>
    382  * <li>2:	standard deviation of these values</li>
    383  * <li>3:	coefficient of variation of these values</li>
    384  * </ul>
    385  */
    386 public double[] getStatistics() {
    387 	return getStatistics(Util.ALL_BUILD_PREFIXES, DB_Results.getDefaultDimension().getId());
    388 }
    389 
    390 /**
    391  * Get all dimension builds default dimension statistics for a given list of build
    392  * prefixes.
    393  *
    394  * @param prefixes List of prefixes to filter builds. If <code>null</code>
    395  * 	then all the builds are taken to compute statistics.
    396  * @return An array of double built as follows:
    397  * <ul>
    398  * <li>0:	numbers of values</li>
    399  * <li>1:	mean of values</li>
    400  * <li>2:	standard deviation of these values</li>
    401  * <li>3:	coefficient of variation of these values</li>
    402  * </ul>
    403  */
    404 public double[] getStatistics(List prefixes) {
    405 	return getStatistics(prefixes, DB_Results.getDefaultDimension().getId());
    406 }
    407 
    408 /**
    409  * Get all dimension builds statistics for a given list of build prefixes
    410  * and a given dimension.
    411  *
    412  * @param prefixes List of prefixes to filter builds. If <code>null</code>
    413  * 	then all the builds are taken to compute statistics.
    414  * @param dim_id The id of the dimension on which the statistics must be computed
    415  * @return An array of double built as follows:
    416  * <ul>
    417  * <li>0:	numbers of values</li>
    418  * <li>1:	mean of values</li>
    419  * <li>2:	standard deviation of these values</li>
    420  * <li>3:	coefficient of variation of these values</li>
    421  * </ul>
    422  */
    423 public double[] getStatistics(List prefixes, int dim_id) {
    424 	int size = size();
    425 	int length = prefixes == null ? 0 : prefixes.size();
    426 	int count = 0;
    427 	double mean=0, stddev=0, variation=0;
    428 	double[] values = new double[size];
    429 	count = 0;
    430 	mean = 0.0;
    431 	for (int i=0; i<size; i++) {
    432 		BuildResults buildResults = (BuildResults) this.children.get(i);
    433 		String buildName = buildResults.getName();
    434 		if (isBuildConcerned(buildResults)) {
    435 			if (prefixes == null) {
    436 				double value = buildResults.getValue(dim_id);
    437 				values[count] = value;
    438 				mean += value;
    439 				count++;
    440 			} else {
    441 				for (int j=0; j<length; j++) {
    442 					if (buildName.startsWith((String)prefixes.get(j))) {
    443 						double value = buildResults.getValue(dim_id);
    444 						values[count] = value;
    445 						mean += value;
    446 						count++;
    447 					}
    448 				}
    449 			}
    450 		}
    451 	}
    452 	mean /= count;
    453 	for (int i=0; i<count; i++) {
    454 		stddev += Math.pow(values[i] - mean, 2);
    455 	}
    456 	stddev = Math.sqrt((stddev / (count - 1)));
    457 	variation = stddev / mean;
    458 	return new double[] { count, mean, stddev, variation };
    459 }
    460 
    461 private void initialize() {
    462 	reset();
    463 	// Get performance results builds name
    464 	PerformanceResults perfResults = getPerformance();
    465 	String baselineBuildName = perfResults.getBaselineName();
    466 	String baselineBuildDate = baselineBuildName == null ? null : Util.getBuildDate(baselineBuildName);
    467 	String currentBuildName = perfResults.name;
    468 	String currentBuildDate = currentBuildName == null ? null : Util.getBuildDate(currentBuildName);
    469 
    470 	// Set baseline and current builds
    471 	BuildResults lastBaseline = null;
    472 	int size = size();
    473 	if (size == 0) return;
    474 	for (int i=0; i<size; i++) {
    475 		BuildResults buildResults = (BuildResults) this.children.get(i);
    476 		if (buildResults.values != null) {
    477 			buildResults.cleanValues();
    478 		}
    479 		if (buildResults.isBaseline()) {
    480 			if (lastBaseline == null || baselineBuildDate == null || baselineBuildDate.compareTo(buildResults.getDate()) >= 0) {
    481 				lastBaseline = buildResults;
    482 			}
    483 			if (baselineBuildName != null && buildResults.getName().equals(baselineBuildName)) {
    484 				this.baseline = buildResults;
    485 				this.baselined = true;
    486 			}
    487 		} else if (currentBuildName == null || currentBuildDate == null || (this.current == null && buildResults.getDate().compareTo(currentBuildDate) >= 0)) {
    488 			this.current = buildResults;
    489 			this.valid = true;
    490 		}
    491 	}
    492 	if (this.baseline == null) {
    493 		this.baseline = (lastBaseline == null) ? (BuildResults) this.children.get(0) : lastBaseline;
    494 	}
    495 	if (this.current == null) {
    496 		int idx = size() - 1;
    497 		BuildResults previous = (BuildResults) this.children.get(idx--);
    498 		while (idx >= 0 && previous.isBaseline()) {
    499 			previous = (BuildResults) this.children.get(idx--);
    500 		}
    501 		this.current = previous;
    502 	}
    503 
    504 	// Set delta between current vs. baseline and the corresponding error
    505 	int dim_id = DB_Results.getDefaultDimension().getId();
    506 	double baselineValue = this.baseline.getValue();
    507 	double currentValue = this.current.getValue();
    508 	this.delta = (currentValue - baselineValue) / baselineValue;
    509 	if (Double.isNaN(this.delta)) {
    510 		this.error = Double.NaN;
    511 	} else {
    512 		long baselineCount = this.baseline.getCount(dim_id);
    513 		long currentCount = this.current.getCount(dim_id);
    514 		if (baselineCount == 1 || currentCount == 1) {
    515 			this.error = Double.NaN;
    516 		} else {
    517 			double baselineError = this.baseline.getError(dim_id);
    518 			double currentError = this.current.getError(dim_id);
    519 			this.error = Double.isNaN(baselineError)
    520 					? currentError / baselineValue
    521 					: Math.sqrt(baselineError*baselineError + currentError*currentError) / baselineValue;
    522 		}
    523 	}
    524 
    525 	// Set the failure on the current build if necessary
    526 	int failure_threshold = getPerformance().failure_threshold;
    527 	if (this.delta >= (failure_threshold/100.0)) {
    528 		StringBuffer buffer = new StringBuffer("Performance criteria not met when compared to '"); //$NON-NLS-1$
    529 		buffer.append(this.baseline.getName());
    530 		buffer.append("': "); //$NON-NLS-1$
    531 		buffer.append(DB_Results.getDefaultDimension().getName());
    532 		buffer.append("= "); //$NON-NLS-1$
    533 		buffer.append(Util.timeString((long)this.current.getValue()));
    534 		buffer.append(" is not within [0%, "); //$NON-NLS-1$
    535 		buffer.append(100+failure_threshold);
    536 		buffer.append("'%] of "); //$NON-NLS-1$
    537 		buffer.append(Util.timeString((long)this.baseline.getValue()));
    538 		this.current.setFailure(buffer.toString());
    539 	}
    540 }
    541 
    542 /**
    543  * Returns whether the configuration has results for the performance
    544  * baseline build or not.
    545  *
    546  * @return <code>true</code> if the configuration has results
    547  * 	for the performance baseline build, <code>false</code> otherwise.
    548  */
    549 public boolean isBaselined() {
    550 	if (this.baseline == null || this.current == null) {
    551 	    initialize();
    552     }
    553 	return this.baselined;
    554 }
    555 
    556 boolean isBuildConcerned(BuildResults buildResults) {
    557 	String buildDate = buildResults.getDate();
    558 	String currentBuildDate = getCurrentBuildResults() == null ? null : getCurrentBuildResults().getDate();
    559 	String baselineBuildDate = getBaselineBuildResults() == null ? null : getBaselineBuildResults().getDate();
    560 	return ((currentBuildDate == null || buildDate.compareTo(currentBuildDate) <= 0) &&
    561 		(baselineBuildDate == null || buildDate.compareTo(baselineBuildDate) <= 0));
    562 }
    563 /**
    564  * Returns whether the configuration has results for the performance
    565  * current build or not.
    566  *
    567  * @return <code>true</code> if the configuration has results
    568  * 	for the performance current build, <code>false</code> otherwise.
    569  */
    570 public boolean isValid() {
    571 	if (this.baseline == null || this.current == null) {
    572 	    initialize();
    573     }
    574 	return this.valid;
    575 }
    576 
    577 /**
    578  * Returns the 'n' last nightly build names.
    579  *
    580  * @param n Number of last nightly builds to return
    581  * @return Last n nightly build names preceding current.
    582  */
    583 public List lastNightlyBuildNames(int n) {
    584 	List labels = new ArrayList();
    585 	for (int i=size()-2; i>=0; i--) {
    586 		BuildResults buildResults = (BuildResults) this.children.get(i);
    587 		if (isBuildConcerned(buildResults)) {
    588 			String buildName = buildResults.getName();
    589 			if (buildName.startsWith("N")) { //$NON-NLS-1$
    590 				labels.add(buildName);
    591 				if (labels.size() >= n) {
    592 	                break;
    593                 }
    594 			}
    595 		}
    596 	}
    597 	return labels;
    598 }
    599 
    600 /*
    601  * Read all configuration builds results data from the given stream.
    602  */
    603 void readData(DataInputStream stream) throws IOException {
    604 	int size = stream.readInt();
    605 	for (int i=0; i<size; i++) {
    606 		BuildResults buildResults = new BuildResults(this);
    607 		buildResults.readData(stream);
    608 		String lastBuildName = getPerformance().lastBuildName;
    609 		if (lastBuildName == null || buildResults.getDate().compareTo(Util.getBuildDate(lastBuildName)) <= 0) {
    610 			addChild(buildResults, true);
    611 		}
    612 	}
    613 }
    614 
    615 private void reset() {
    616 	this.current = null;
    617 	this.baseline = null;
    618 	this.baselined = false;
    619 	this.valid = false;
    620 	this.delta = 0;
    621 	this.error = -1;
    622 }
    623 
    624 /*
    625  * Set the configuration value from database information
    626  */
    627 void setInfos(int build_id, int summaryKind, String comment) {
    628 	BuildResults buildResults = (BuildResults) getResults(build_id);
    629 	if (buildResults == null) {
    630 		buildResults = new BuildResults(this, build_id);
    631 		addChild(buildResults, true);
    632 	}
    633 	buildResults.summaryKind = summaryKind;
    634 	buildResults.comment = comment;
    635 }
    636 
    637 /*
    638  * Set the configuration value from database information
    639  */
    640 void setValue(int build_id, int dim_id, int step, long value) {
    641 //	reset();
    642 	BuildResults buildResults = (BuildResults) getResults(build_id);
    643 	if (buildResults == null) {
    644 		buildResults = new BuildResults(this, build_id);
    645 		addChild(buildResults, true);
    646 	}
    647 	buildResults.setValue(dim_id, step, value);
    648 }
    649 
    650 /*
    651  * Write all configuration builds results data into the given stream.
    652  */
    653 void write(DataOutputStream stream) throws IOException {
    654 	int size = size();
    655 	stream.writeInt(this.id);
    656 	stream.writeInt(size);
    657 	for (int i=0; i<size; i++) {
    658 		BuildResults buildResults = (BuildResults) this.children.get(i);
    659 		buildResults.write(stream);
    660 	}
    661 }
    662 
    663 }
    664