1 /*===- InstrProfilingFile.c - Write instrumentation to a file -------------===*\ 2 |* 3 |* The LLVM Compiler Infrastructure 4 |* 5 |* This file is distributed under the University of Illinois Open Source 6 |* License. See LICENSE.TXT for details. 7 |* 8 \*===----------------------------------------------------------------------===*/ 9 10 #include <errno.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 #ifdef _MSC_VER 15 /* For _alloca. */ 16 #include <malloc.h> 17 #endif 18 #if defined(_WIN32) 19 #include "WindowsMMap.h" 20 /* For _chsize_s */ 21 #include <io.h> 22 #else 23 #include <sys/file.h> 24 #include <sys/mman.h> 25 #include <unistd.h> 26 #if defined(__linux__) 27 #include <sys/types.h> 28 #endif 29 #endif 30 31 #include "InstrProfiling.h" 32 #include "InstrProfilingInternal.h" 33 #include "InstrProfilingUtil.h" 34 35 /* From where is profile name specified. 36 * The order the enumerators define their 37 * precedence. Re-order them may lead to 38 * runtime behavior change. */ 39 typedef enum ProfileNameSpecifier { 40 PNS_unknown = 0, 41 PNS_default, 42 PNS_command_line, 43 PNS_environment, 44 PNS_runtime_api 45 } ProfileNameSpecifier; 46 47 static const char *getPNSStr(ProfileNameSpecifier PNS) { 48 switch (PNS) { 49 case PNS_default: 50 return "default setting"; 51 case PNS_command_line: 52 return "command line"; 53 case PNS_environment: 54 return "environment variable"; 55 case PNS_runtime_api: 56 return "runtime API"; 57 default: 58 return "Unknown"; 59 } 60 } 61 62 #define MAX_PID_SIZE 16 63 /* Data structure holding the result of parsed filename pattern. */ 64 typedef struct lprofFilename { 65 /* File name string possibly with %p or %h specifiers. */ 66 const char *FilenamePat; 67 /* A flag indicating if FilenamePat's memory is allocated 68 * by runtime. */ 69 unsigned OwnsFilenamePat; 70 const char *ProfilePathPrefix; 71 char PidChars[MAX_PID_SIZE]; 72 char Hostname[COMPILER_RT_MAX_HOSTLEN]; 73 unsigned NumPids; 74 unsigned NumHosts; 75 /* When in-process merging is enabled, this parameter specifies 76 * the total number of profile data files shared by all the processes 77 * spawned from the same binary. By default the value is 1. If merging 78 * is not enabled, its value should be 0. This parameter is specified 79 * by the %[0-9]m specifier. For instance %2m enables merging using 80 * 2 profile data files. %1m is equivalent to %m. Also %m specifier 81 * can only appear once at the end of the name pattern. */ 82 unsigned MergePoolSize; 83 ProfileNameSpecifier PNS; 84 } lprofFilename; 85 86 COMPILER_RT_WEAK lprofFilename lprofCurFilename = {0, 0, 0, {0}, {0}, 87 0, 0, 0, PNS_unknown}; 88 89 static int getCurFilenameLength(); 90 static const char *getCurFilename(char *FilenameBuf); 91 static unsigned doMerging() { return lprofCurFilename.MergePoolSize; } 92 93 /* Return 1 if there is an error, otherwise return 0. */ 94 static uint32_t fileWriter(ProfDataWriter *This, ProfDataIOVec *IOVecs, 95 uint32_t NumIOVecs) { 96 uint32_t I; 97 FILE *File = (FILE *)This->WriterCtx; 98 for (I = 0; I < NumIOVecs; I++) { 99 if (IOVecs[I].Data) { 100 if (fwrite(IOVecs[I].Data, IOVecs[I].ElmSize, IOVecs[I].NumElm, File) != 101 IOVecs[I].NumElm) 102 return 1; 103 } else { 104 if (fseek(File, IOVecs[I].ElmSize * IOVecs[I].NumElm, SEEK_CUR) == -1) 105 return 1; 106 } 107 } 108 return 0; 109 } 110 111 static void initFileWriter(ProfDataWriter *This, FILE *File) { 112 This->Write = fileWriter; 113 This->WriterCtx = File; 114 } 115 116 COMPILER_RT_VISIBILITY ProfBufferIO * 117 lprofCreateBufferIOInternal(void *File, uint32_t BufferSz) { 118 FreeHook = &free; 119 DynamicBufferIOBuffer = (uint8_t *)calloc(BufferSz, 1); 120 VPBufferSize = BufferSz; 121 ProfDataWriter *fileWriter = 122 (ProfDataWriter *)calloc(sizeof(ProfDataWriter), 1); 123 initFileWriter(fileWriter, File); 124 ProfBufferIO *IO = lprofCreateBufferIO(fileWriter); 125 IO->OwnFileWriter = 1; 126 return IO; 127 } 128 129 static void setupIOBuffer() { 130 const char *BufferSzStr = 0; 131 BufferSzStr = getenv("LLVM_VP_BUFFER_SIZE"); 132 if (BufferSzStr && BufferSzStr[0]) { 133 VPBufferSize = atoi(BufferSzStr); 134 DynamicBufferIOBuffer = (uint8_t *)calloc(VPBufferSize, 1); 135 } 136 } 137 138 /* Read profile data in \c ProfileFile and merge with in-memory 139 profile counters. Returns -1 if there is fatal error, otheriwse 140 0 is returned. Returning 0 does not mean merge is actually 141 performed. If merge is actually done, *MergeDone is set to 1. 142 */ 143 static int doProfileMerging(FILE *ProfileFile, int *MergeDone) { 144 uint64_t ProfileFileSize; 145 char *ProfileBuffer; 146 147 if (fseek(ProfileFile, 0L, SEEK_END) == -1) { 148 PROF_ERR("Unable to merge profile data, unable to get size: %s\n", 149 strerror(errno)); 150 return -1; 151 } 152 ProfileFileSize = ftell(ProfileFile); 153 154 /* Restore file offset. */ 155 if (fseek(ProfileFile, 0L, SEEK_SET) == -1) { 156 PROF_ERR("Unable to merge profile data, unable to rewind: %s\n", 157 strerror(errno)); 158 return -1; 159 } 160 161 /* Nothing to merge. */ 162 if (ProfileFileSize < sizeof(__llvm_profile_header)) { 163 if (ProfileFileSize) 164 PROF_WARN("Unable to merge profile data: %s\n", 165 "source profile file is too small."); 166 return 0; 167 } 168 169 ProfileBuffer = mmap(NULL, ProfileFileSize, PROT_READ, MAP_SHARED | MAP_FILE, 170 fileno(ProfileFile), 0); 171 if (ProfileBuffer == MAP_FAILED) { 172 PROF_ERR("Unable to merge profile data, mmap failed: %s\n", 173 strerror(errno)); 174 return -1; 175 } 176 177 if (__llvm_profile_check_compatibility(ProfileBuffer, ProfileFileSize)) { 178 (void)munmap(ProfileBuffer, ProfileFileSize); 179 PROF_WARN("Unable to merge profile data: %s\n", 180 "source profile file is not compatible."); 181 return 0; 182 } 183 184 /* Now start merging */ 185 __llvm_profile_merge_from_buffer(ProfileBuffer, ProfileFileSize); 186 187 // Truncate the file in case merging of value profile did not happend to 188 // prevent from leaving garbage data at the end of the profile file. 189 ftruncate(fileno(ProfileFile), __llvm_profile_get_size_for_buffer()); 190 191 (void)munmap(ProfileBuffer, ProfileFileSize); 192 *MergeDone = 1; 193 194 return 0; 195 } 196 197 /* Create the directory holding the file, if needed. */ 198 static void createProfileDir(const char *Filename) { 199 size_t Length = strlen(Filename); 200 if (lprofFindFirstDirSeparator(Filename)) { 201 char *Copy = (char *)COMPILER_RT_ALLOCA(Length + 1); 202 strncpy(Copy, Filename, Length + 1); 203 __llvm_profile_recursive_mkdir(Copy); 204 } 205 } 206 207 /* Open the profile data for merging. It opens the file in r+b mode with 208 * file locking. If the file has content which is compatible with the 209 * current process, it also reads in the profile data in the file and merge 210 * it with in-memory counters. After the profile data is merged in memory, 211 * the original profile data is truncated and gets ready for the profile 212 * dumper. With profile merging enabled, each executable as well as any of 213 * its instrumented shared libraries dump profile data into their own data file. 214 */ 215 static FILE *openFileForMerging(const char *ProfileFileName, int *MergeDone) { 216 FILE *ProfileFile; 217 int rc; 218 219 createProfileDir(ProfileFileName); 220 ProfileFile = lprofOpenFileEx(ProfileFileName); 221 if (!ProfileFile) 222 return NULL; 223 224 rc = doProfileMerging(ProfileFile, MergeDone); 225 if (rc || (!*MergeDone && COMPILER_RT_FTRUNCATE(ProfileFile, 0L)) || 226 fseek(ProfileFile, 0L, SEEK_SET) == -1) { 227 PROF_ERR("Profile Merging of file %s failed: %s\n", ProfileFileName, 228 strerror(errno)); 229 fclose(ProfileFile); 230 return NULL; 231 } 232 return ProfileFile; 233 } 234 235 /* Write profile data to file \c OutputName. */ 236 static int writeFile(const char *OutputName) { 237 int RetVal; 238 FILE *OutputFile; 239 240 int MergeDone = 0; 241 VPMergeHook = &lprofMergeValueProfData; 242 if (!doMerging()) 243 OutputFile = fopen(OutputName, "ab"); 244 else 245 OutputFile = openFileForMerging(OutputName, &MergeDone); 246 247 if (!OutputFile) 248 return -1; 249 250 FreeHook = &free; 251 setupIOBuffer(); 252 ProfDataWriter fileWriter; 253 initFileWriter(&fileWriter, OutputFile); 254 RetVal = lprofWriteData(&fileWriter, lprofGetVPDataReader(), MergeDone); 255 256 fclose(OutputFile); 257 return RetVal; 258 } 259 260 static void truncateCurrentFile(void) { 261 const char *Filename; 262 char *FilenameBuf; 263 FILE *File; 264 int Length; 265 266 Length = getCurFilenameLength(); 267 FilenameBuf = (char *)COMPILER_RT_ALLOCA(Length + 1); 268 Filename = getCurFilename(FilenameBuf); 269 if (!Filename) 270 return; 271 272 /* By pass file truncation to allow online raw profile 273 * merging. */ 274 if (lprofCurFilename.MergePoolSize) 275 return; 276 277 createProfileDir(Filename); 278 279 /* Truncate the file. Later we'll reopen and append. */ 280 File = fopen(Filename, "w"); 281 if (!File) 282 return; 283 fclose(File); 284 } 285 286 static const char *DefaultProfileName = "default.profraw"; 287 static void resetFilenameToDefault(void) { 288 if (lprofCurFilename.FilenamePat && lprofCurFilename.OwnsFilenamePat) { 289 free((void *)lprofCurFilename.FilenamePat); 290 } 291 memset(&lprofCurFilename, 0, sizeof(lprofCurFilename)); 292 lprofCurFilename.FilenamePat = DefaultProfileName; 293 lprofCurFilename.PNS = PNS_default; 294 } 295 296 static int containsMergeSpecifier(const char *FilenamePat, int I) { 297 return (FilenamePat[I] == 'm' || 298 (FilenamePat[I] >= '1' && FilenamePat[I] <= '9' && 299 /* If FilenamePat[I] is not '\0', the next byte is guaranteed 300 * to be in-bound as the string is null terminated. */ 301 FilenamePat[I + 1] == 'm')); 302 } 303 304 /* Parses the pattern string \p FilenamePat and stores the result to 305 * lprofcurFilename structure. */ 306 static int parseFilenamePattern(const char *FilenamePat, 307 unsigned CopyFilenamePat) { 308 int NumPids = 0, NumHosts = 0, I; 309 char *PidChars = &lprofCurFilename.PidChars[0]; 310 char *Hostname = &lprofCurFilename.Hostname[0]; 311 int MergingEnabled = 0; 312 313 /* Clean up cached prefix. */ 314 if (lprofCurFilename.ProfilePathPrefix) 315 free((void *)lprofCurFilename.ProfilePathPrefix); 316 memset(&lprofCurFilename, 0, sizeof(lprofCurFilename)); 317 318 if (lprofCurFilename.FilenamePat && lprofCurFilename.OwnsFilenamePat) { 319 free((void *)lprofCurFilename.FilenamePat); 320 } 321 322 if (!CopyFilenamePat) 323 lprofCurFilename.FilenamePat = FilenamePat; 324 else { 325 lprofCurFilename.FilenamePat = strdup(FilenamePat); 326 lprofCurFilename.OwnsFilenamePat = 1; 327 } 328 /* Check the filename for "%p", which indicates a pid-substitution. */ 329 for (I = 0; FilenamePat[I]; ++I) 330 if (FilenamePat[I] == '%') { 331 if (FilenamePat[++I] == 'p') { 332 if (!NumPids++) { 333 if (snprintf(PidChars, MAX_PID_SIZE, "%ld", (long)getpid()) <= 0) { 334 PROF_WARN("Unable to get pid for filename pattern %s. Using the " 335 "default name.", 336 FilenamePat); 337 return -1; 338 } 339 } 340 } else if (FilenamePat[I] == 'h') { 341 if (!NumHosts++) 342 if (COMPILER_RT_GETHOSTNAME(Hostname, COMPILER_RT_MAX_HOSTLEN)) { 343 PROF_WARN("Unable to get hostname for filename pattern %s. Using " 344 "the default name.", 345 FilenamePat); 346 return -1; 347 } 348 } else if (containsMergeSpecifier(FilenamePat, I)) { 349 if (MergingEnabled) { 350 PROF_WARN("%%m specifier can only be specified once in %s.\n", 351 FilenamePat); 352 return -1; 353 } 354 MergingEnabled = 1; 355 if (FilenamePat[I] == 'm') 356 lprofCurFilename.MergePoolSize = 1; 357 else { 358 lprofCurFilename.MergePoolSize = FilenamePat[I] - '0'; 359 I++; /* advance to 'm' */ 360 } 361 } 362 } 363 364 lprofCurFilename.NumPids = NumPids; 365 lprofCurFilename.NumHosts = NumHosts; 366 return 0; 367 } 368 369 static void parseAndSetFilename(const char *FilenamePat, 370 ProfileNameSpecifier PNS, 371 unsigned CopyFilenamePat) { 372 373 const char *OldFilenamePat = lprofCurFilename.FilenamePat; 374 ProfileNameSpecifier OldPNS = lprofCurFilename.PNS; 375 376 if (PNS < OldPNS) 377 return; 378 379 if (!FilenamePat) 380 FilenamePat = DefaultProfileName; 381 382 if (OldFilenamePat && !strcmp(OldFilenamePat, FilenamePat)) { 383 lprofCurFilename.PNS = PNS; 384 return; 385 } 386 387 /* When PNS >= OldPNS, the last one wins. */ 388 if (!FilenamePat || parseFilenamePattern(FilenamePat, CopyFilenamePat)) 389 resetFilenameToDefault(); 390 lprofCurFilename.PNS = PNS; 391 392 if (!OldFilenamePat) { 393 if (getenv("LLVM_PROFILE_VERBOSE")) 394 PROF_NOTE("Set profile file path to \"%s\" via %s.\n", 395 lprofCurFilename.FilenamePat, getPNSStr(PNS)); 396 } else { 397 if (getenv("LLVM_PROFILE_VERBOSE")) 398 PROF_NOTE("Override old profile path \"%s\" via %s to \"%s\" via %s.\n", 399 OldFilenamePat, getPNSStr(OldPNS), lprofCurFilename.FilenamePat, 400 getPNSStr(PNS)); 401 } 402 403 truncateCurrentFile(); 404 } 405 406 /* Return buffer length that is required to store the current profile 407 * filename with PID and hostname substitutions. */ 408 /* The length to hold uint64_t followed by 2 digit pool id including '_' */ 409 #define SIGLEN 24 410 static int getCurFilenameLength() { 411 int Len; 412 if (!lprofCurFilename.FilenamePat || !lprofCurFilename.FilenamePat[0]) 413 return 0; 414 415 if (!(lprofCurFilename.NumPids || lprofCurFilename.NumHosts || 416 lprofCurFilename.MergePoolSize)) 417 return strlen(lprofCurFilename.FilenamePat); 418 419 Len = strlen(lprofCurFilename.FilenamePat) + 420 lprofCurFilename.NumPids * (strlen(lprofCurFilename.PidChars) - 2) + 421 lprofCurFilename.NumHosts * (strlen(lprofCurFilename.Hostname) - 2); 422 if (lprofCurFilename.MergePoolSize) 423 Len += SIGLEN; 424 return Len; 425 } 426 427 /* Return the pointer to the current profile file name (after substituting 428 * PIDs and Hostnames in filename pattern. \p FilenameBuf is the buffer 429 * to store the resulting filename. If no substitution is needed, the 430 * current filename pattern string is directly returned. */ 431 static const char *getCurFilename(char *FilenameBuf) { 432 int I, J, PidLength, HostNameLength; 433 const char *FilenamePat = lprofCurFilename.FilenamePat; 434 435 if (!lprofCurFilename.FilenamePat || !lprofCurFilename.FilenamePat[0]) 436 return 0; 437 438 if (!(lprofCurFilename.NumPids || lprofCurFilename.NumHosts || 439 lprofCurFilename.MergePoolSize)) 440 return lprofCurFilename.FilenamePat; 441 442 PidLength = strlen(lprofCurFilename.PidChars); 443 HostNameLength = strlen(lprofCurFilename.Hostname); 444 /* Construct the new filename. */ 445 for (I = 0, J = 0; FilenamePat[I]; ++I) 446 if (FilenamePat[I] == '%') { 447 if (FilenamePat[++I] == 'p') { 448 memcpy(FilenameBuf + J, lprofCurFilename.PidChars, PidLength); 449 J += PidLength; 450 } else if (FilenamePat[I] == 'h') { 451 memcpy(FilenameBuf + J, lprofCurFilename.Hostname, HostNameLength); 452 J += HostNameLength; 453 } else if (containsMergeSpecifier(FilenamePat, I)) { 454 char LoadModuleSignature[SIGLEN]; 455 int S; 456 int ProfilePoolId = getpid() % lprofCurFilename.MergePoolSize; 457 S = snprintf(LoadModuleSignature, SIGLEN, "%" PRIu64 "_%d", 458 lprofGetLoadModuleSignature(), ProfilePoolId); 459 if (S == -1 || S > SIGLEN) 460 S = SIGLEN; 461 memcpy(FilenameBuf + J, LoadModuleSignature, S); 462 J += S; 463 if (FilenamePat[I] != 'm') 464 I++; 465 } 466 /* Drop any unknown substitutions. */ 467 } else 468 FilenameBuf[J++] = FilenamePat[I]; 469 FilenameBuf[J] = 0; 470 471 return FilenameBuf; 472 } 473 474 /* Returns the pointer to the environment variable 475 * string. Returns null if the env var is not set. */ 476 static const char *getFilenamePatFromEnv(void) { 477 const char *Filename = getenv("LLVM_PROFILE_FILE"); 478 if (!Filename || !Filename[0]) 479 return 0; 480 return Filename; 481 } 482 483 COMPILER_RT_VISIBILITY 484 const char *__llvm_profile_get_path_prefix(void) { 485 int Length; 486 char *FilenameBuf, *Prefix; 487 const char *Filename, *PrefixEnd; 488 489 if (lprofCurFilename.ProfilePathPrefix) 490 return lprofCurFilename.ProfilePathPrefix; 491 492 Length = getCurFilenameLength(); 493 FilenameBuf = (char *)COMPILER_RT_ALLOCA(Length + 1); 494 Filename = getCurFilename(FilenameBuf); 495 if (!Filename) 496 return "\0"; 497 498 PrefixEnd = lprofFindLastDirSeparator(Filename); 499 if (!PrefixEnd) 500 return "\0"; 501 502 Length = PrefixEnd - Filename + 1; 503 Prefix = (char *)malloc(Length + 1); 504 if (!Prefix) { 505 PROF_ERR("Failed to %s\n", "allocate memory."); 506 return "\0"; 507 } 508 memcpy(Prefix, Filename, Length); 509 Prefix[Length] = '\0'; 510 lprofCurFilename.ProfilePathPrefix = Prefix; 511 return Prefix; 512 } 513 514 /* This method is invoked by the runtime initialization hook 515 * InstrProfilingRuntime.o if it is linked in. Both user specified 516 * profile path via -fprofile-instr-generate= and LLVM_PROFILE_FILE 517 * environment variable can override this default value. */ 518 COMPILER_RT_VISIBILITY 519 void __llvm_profile_initialize_file(void) { 520 const char *EnvFilenamePat; 521 const char *SelectedPat = NULL; 522 ProfileNameSpecifier PNS = PNS_unknown; 523 int hasCommandLineOverrider = (INSTR_PROF_PROFILE_NAME_VAR[0] != 0); 524 525 EnvFilenamePat = getFilenamePatFromEnv(); 526 if (EnvFilenamePat) { 527 /* Pass CopyFilenamePat = 1, to ensure that the filename would be valid 528 at the moment when __llvm_profile_write_file() gets executed. */ 529 parseAndSetFilename(EnvFilenamePat, PNS_environment, 1); 530 return; 531 } else if (hasCommandLineOverrider) { 532 SelectedPat = INSTR_PROF_PROFILE_NAME_VAR; 533 PNS = PNS_command_line; 534 } else { 535 SelectedPat = NULL; 536 PNS = PNS_default; 537 } 538 539 parseAndSetFilename(SelectedPat, PNS, 0); 540 } 541 542 /* This API is directly called by the user application code. It has the 543 * highest precedence compared with LLVM_PROFILE_FILE environment variable 544 * and command line option -fprofile-instr-generate=<profile_name>. 545 */ 546 COMPILER_RT_VISIBILITY 547 void __llvm_profile_set_filename(const char *FilenamePat) { 548 parseAndSetFilename(FilenamePat, PNS_runtime_api, 1); 549 } 550 551 /* The public API for writing profile data into the file with name 552 * set by previous calls to __llvm_profile_set_filename or 553 * __llvm_profile_override_default_filename or 554 * __llvm_profile_initialize_file. */ 555 COMPILER_RT_VISIBILITY 556 int __llvm_profile_write_file(void) { 557 int rc, Length; 558 const char *Filename; 559 char *FilenameBuf; 560 int PDeathSig = 0; 561 562 if (lprofProfileDumped()) { 563 PROF_NOTE("Profile data not written to file: %s.\n", 564 "already written"); 565 return 0; 566 } 567 568 Length = getCurFilenameLength(); 569 FilenameBuf = (char *)COMPILER_RT_ALLOCA(Length + 1); 570 Filename = getCurFilename(FilenameBuf); 571 572 /* Check the filename. */ 573 if (!Filename) { 574 PROF_ERR("Failed to write file : %s\n", "Filename not set"); 575 return -1; 576 } 577 578 /* Check if there is llvm/runtime version mismatch. */ 579 if (GET_VERSION(__llvm_profile_get_version()) != INSTR_PROF_RAW_VERSION) { 580 PROF_ERR("Runtime and instrumentation version mismatch : " 581 "expected %d, but get %d\n", 582 INSTR_PROF_RAW_VERSION, 583 (int)GET_VERSION(__llvm_profile_get_version())); 584 return -1; 585 } 586 587 // Temporarily suspend getting SIGKILL when the parent exits. 588 PDeathSig = lprofSuspendSigKill(); 589 590 /* Write profile data to the file. */ 591 rc = writeFile(Filename); 592 if (rc) 593 PROF_ERR("Failed to write file \"%s\": %s\n", Filename, strerror(errno)); 594 595 // Restore SIGKILL. 596 if (PDeathSig == 1) 597 lprofRestoreSigKill(); 598 599 return rc; 600 } 601 602 COMPILER_RT_VISIBILITY 603 int __llvm_profile_dump(void) { 604 if (!doMerging()) 605 PROF_WARN("Later invocation of __llvm_profile_dump can lead to clobbering " 606 " of previously dumped profile data : %s. Either use %%m " 607 "in profile name or change profile name before dumping.\n", 608 "online profile merging is not on"); 609 int rc = __llvm_profile_write_file(); 610 lprofSetProfileDumped(); 611 return rc; 612 } 613 614 static void writeFileWithoutReturn(void) { __llvm_profile_write_file(); } 615 616 COMPILER_RT_VISIBILITY 617 int __llvm_profile_register_write_file_atexit(void) { 618 static int HasBeenRegistered = 0; 619 620 if (HasBeenRegistered) 621 return 0; 622 623 lprofSetupValueProfiler(); 624 625 HasBeenRegistered = 1; 626 return atexit(writeFileWithoutReturn); 627 } 628