1 /* 2 * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 * 23 */ 24 25 #include "precompiled.hpp" 26 #include "jfr/recorder/checkpoint/constant/jfrTagSet.hpp" 27 #include "jfr/recorder/checkpoint/constant/jfrTagSetWriter.hpp" 28 #include "jfr/leakprofiler/chains/edge.hpp" 29 #include "jfr/leakprofiler/chains/edgeStore.hpp" 30 #include "jfr/leakprofiler/chains/edgeUtils.hpp" 31 #include "jfr/leakprofiler/checkpoint/objectSampleDescription.hpp" 32 #include "jfr/leakprofiler/checkpoint/objectSampleWriter.hpp" 33 #include "jfr/leakprofiler/checkpoint/rootResolver.hpp" 34 #include "jfr/leakprofiler/sampling/objectSampler.hpp" 35 #include "jfr/leakprofiler/utilities/rootType.hpp" 36 #include "jfr/leakprofiler/utilities/unifiedOop.hpp" 37 #include "oops/oop.inline.hpp" 38 #include "oops/symbol.hpp" 39 #include "tracefiles/traceTypes.hpp" 40 #include "utilities/growableArray.hpp" 41 42 template <typename Data> 43 class ObjectSampleAuxInfo : public ResourceObj { 44 public: 45 Data _data; 46 traceid _id; 47 ObjectSampleAuxInfo() : _data(), _id(0) {} 48 }; 49 50 class ObjectSampleArrayData { 51 public: 52 int _array_size; 53 int _array_index; 54 ObjectSampleArrayData() : _array_size(0), _array_index(0) {} 55 }; 56 57 class ObjectSampleFieldInfo : public ResourceObj { 58 public: 59 const Symbol* _field_name_symbol; 60 jshort _field_modifiers; 61 ObjectSampleFieldInfo() : _field_name_symbol(NULL), _field_modifiers(0) {} 62 }; 63 64 class ObjectSampleRootDescriptionData { 65 public: 66 const Edge* _root_edge; 67 const char* _description; 68 OldObjectRoot::System _system; 69 OldObjectRoot::Type _type; 70 ObjectSampleRootDescriptionData() : _root_edge(NULL), 71 _description(NULL), 72 _system(OldObjectRoot::_system_undetermined), 73 _type(OldObjectRoot::_type_undetermined) {} 74 }; 75 76 class OldObjectSampleData { 77 public: 78 oop _object; 79 traceid _reference_id; 80 }; 81 82 class ReferenceData { 83 public: 84 traceid _field_info_id; 85 traceid _array_info_id; 86 traceid _old_object_sample_id; 87 size_t _skip; 88 }; 89 90 static int initial_storage_size = 16; 91 92 template <typename Data> 93 class SampleSet : public ResourceObj { 94 private: 95 GrowableArray<Data>* _storage; 96 public: 97 SampleSet() : _storage(NULL) {} 98 99 traceid store(Data data) { 100 assert(data != NULL, "invariant"); 101 if (_storage == NULL) { 102 _storage = new GrowableArray<Data>(initial_storage_size); 103 } 104 assert(_storage != NULL, "invariant"); 105 assert(_storage->find(data) == -1, "invariant"); 106 _storage->append(data); 107 return data->_id; 108 } 109 110 size_t size() const { 111 return _storage != NULL ? (size_t)_storage->length() : 0; 112 } 113 114 template <typename Functor> 115 void iterate(Functor& functor) { 116 if (_storage != NULL) { 117 for (int i = 0; i < _storage->length(); ++i) { 118 functor(_storage->at(i)); 119 } 120 } 121 } 122 123 const GrowableArray<Data>& storage() const { 124 return *_storage; 125 } 126 }; 127 128 typedef ObjectSampleAuxInfo<ObjectSampleArrayData> ObjectSampleArrayInfo; 129 typedef ObjectSampleAuxInfo<ObjectSampleRootDescriptionData> ObjectSampleRootDescriptionInfo; 130 typedef ObjectSampleAuxInfo<OldObjectSampleData> OldObjectSampleInfo; 131 typedef ObjectSampleAuxInfo<ReferenceData> ReferenceInfo; 132 133 class FieldTable : public ResourceObj { 134 template <typename, 135 typename, 136 template<typename, typename> class, 137 typename, 138 size_t> 139 friend class HashTableHost; 140 typedef HashTableHost<const ObjectSampleFieldInfo*, traceid, Entry, FieldTable, 109> FieldInfoTable; 141 public: 142 typedef FieldInfoTable::HashEntry FieldInfoEntry; 143 144 private: 145 static traceid _field_id_counter; 146 FieldInfoTable* _table; 147 148 void assign_id(FieldInfoEntry* entry) { 149 assert(entry != NULL, "invariant"); 150 entry->set_id(++_field_id_counter); 151 } 152 153 bool equals(const ObjectSampleFieldInfo* query, uintptr_t hash, const FieldInfoEntry* entry) { 154 assert(hash == entry->hash(), "invariant"); 155 assert(query != NULL, "invariant"); 156 const ObjectSampleFieldInfo* stored = entry->literal(); 157 assert(stored != NULL, "invariant"); 158 assert(stored->_field_name_symbol->identity_hash() == query->_field_name_symbol->identity_hash(), "invariant"); 159 return stored->_field_modifiers == query->_field_modifiers; 160 } 161 162 public: 163 FieldTable() : _table(new FieldInfoTable(this)) {} 164 ~FieldTable() { 165 assert(_table != NULL, "invariant"); 166 delete _table; 167 } 168 169 traceid store(const ObjectSampleFieldInfo* field_info) { 170 assert(field_info != NULL, "invariant"); 171 const FieldInfoEntry& entry =_table->lookup_put(field_info, 172 field_info->_field_name_symbol->identity_hash()); 173 return entry.id(); 174 } 175 176 size_t size() const { 177 return _table->cardinality(); 178 } 179 180 template <typename T> 181 void iterate(T& functor) const { 182 _table->iterate_entry<T>(functor); 183 } 184 }; 185 186 traceid FieldTable::_field_id_counter = 0; 187 188 typedef SampleSet<const OldObjectSampleInfo*> SampleInfo; 189 typedef SampleSet<const ReferenceInfo*> RefInfo; 190 typedef SampleSet<const ObjectSampleArrayInfo*> ArrayInfo; 191 typedef SampleSet<const ObjectSampleRootDescriptionInfo*> RootDescriptionInfo; 192 193 static SampleInfo* sample_infos = NULL; 194 static RefInfo* ref_infos = NULL; 195 static ArrayInfo* array_infos = NULL; 196 static FieldTable* field_infos = NULL; 197 static RootDescriptionInfo* root_infos = NULL; 198 199 int __write_sample_info__(JfrCheckpointWriter* writer, JfrArtifactSet* unused, const void* si) { 200 assert(writer != NULL, "invariant"); 201 assert(si != NULL, "invariant"); 202 const OldObjectSampleInfo* const oosi = (const OldObjectSampleInfo*)si; 203 oop object = oosi->_data._object; 204 assert(object != NULL, "invariant"); 205 writer->write(oosi->_id); 206 writer->write((u8)(const HeapWord*)object); 207 writer->write(const_cast<const Klass*>(object->klass())); 208 ObjectSampleDescription od(object); 209 writer->write(od.description()); 210 writer->write(oosi->_data._reference_id); 211 return 1; 212 } 213 214 typedef JfrArtifactWriterImplHost<const OldObjectSampleInfo*, __write_sample_info__> SampleWriterImpl; 215 typedef JfrArtifactWriterHost<SampleWriterImpl, CONSTANT_TYPE_OLDOBJECT> SampleWriter; 216 217 static void write_sample_infos(JfrCheckpointWriter& writer) { 218 if (sample_infos != NULL) { 219 SampleWriter sw(&writer, NULL, false); 220 sample_infos->iterate(sw); 221 } 222 } 223 224 int __write_reference_info__(JfrCheckpointWriter* writer, JfrArtifactSet* unused, const void* ri) { 225 assert(writer != NULL, "invariant"); 226 assert(ri != NULL, "invariant"); 227 const ReferenceInfo* const ref_info = (const ReferenceInfo*)ri; 228 writer->write(ref_info->_id); 229 writer->write(ref_info->_data._array_info_id); 230 writer->write(ref_info->_data._field_info_id); 231 writer->write(ref_info->_data._old_object_sample_id); 232 writer->write<s4>((s4)ref_info->_data._skip); 233 return 1; 234 } 235 236 typedef JfrArtifactWriterImplHost<const ReferenceInfo*, __write_reference_info__> ReferenceWriterImpl; 237 typedef JfrArtifactWriterHost<ReferenceWriterImpl, CONSTANT_TYPE_REFERENCE> ReferenceWriter; 238 239 static void write_reference_infos(JfrCheckpointWriter& writer) { 240 if (ref_infos != NULL) { 241 ReferenceWriter rw(&writer, NULL, false); 242 ref_infos->iterate(rw); 243 } 244 } 245 246 int __write_array_info__(JfrCheckpointWriter* writer, JfrArtifactSet* unused, const void* ai) { 247 assert(writer != NULL, "invariant"); 248 assert(ai != NULL, "invariant"); 249 const ObjectSampleArrayInfo* const osai = (const ObjectSampleArrayInfo*)ai; 250 writer->write(osai->_id); 251 writer->write(osai->_data._array_size); 252 writer->write(osai->_data._array_index); 253 return 1; 254 } 255 256 static traceid get_array_info_id(const Edge& edge, traceid id) { 257 if (edge.is_root() || !EdgeUtils::is_array_element(edge)) { 258 return 0; 259 } 260 if (array_infos == NULL) { 261 array_infos = new ArrayInfo(); 262 } 263 assert(array_infos != NULL, "invariant"); 264 265 ObjectSampleArrayInfo* const osai = new ObjectSampleArrayInfo(); 266 assert(osai != NULL, "invariant"); 267 osai->_id = id; 268 osai->_data._array_size = EdgeUtils::array_size(edge); 269 osai->_data._array_index = EdgeUtils::array_index(edge); 270 return array_infos->store(osai); 271 } 272 273 typedef JfrArtifactWriterImplHost<const ObjectSampleArrayInfo*, __write_array_info__> ArrayWriterImpl; 274 typedef JfrArtifactWriterHost<ArrayWriterImpl, CONSTANT_TYPE_OLDOBJECTARRAY> ArrayWriter; 275 276 static void write_array_infos(JfrCheckpointWriter& writer) { 277 if (array_infos != NULL) { 278 ArrayWriter aw(&writer, NULL, false); 279 array_infos->iterate(aw); 280 } 281 } 282 283 int __write_field_info__(JfrCheckpointWriter* writer, JfrArtifactSet* unused, const void* fi) { 284 assert(writer != NULL, "invariant"); 285 assert(fi != NULL, "invariant"); 286 const FieldTable::FieldInfoEntry* field_info_entry = (const FieldTable::FieldInfoEntry*)fi; 287 writer->write(field_info_entry->id()); 288 const ObjectSampleFieldInfo* const osfi = field_info_entry->literal(); 289 writer->write(osfi->_field_name_symbol->as_C_string()); 290 writer->write(osfi->_field_modifiers); 291 return 1; 292 } 293 294 static traceid get_field_info_id(const Edge& edge) { 295 if (edge.is_root()) { 296 return 0; 297 } 298 299 assert(!EdgeUtils::is_array_element(edge), "invariant"); 300 const Symbol* const field_name_symbol = EdgeUtils::field_name_symbol(edge); 301 if (field_name_symbol == NULL) { 302 return 0; 303 } 304 305 if (field_infos == NULL) { 306 field_infos = new FieldTable(); 307 } 308 assert(field_infos != NULL, "invariant"); 309 310 ObjectSampleFieldInfo* const osfi = new ObjectSampleFieldInfo(); 311 assert(osfi != NULL, "invariant"); 312 osfi->_field_name_symbol = field_name_symbol; 313 osfi->_field_modifiers = EdgeUtils::field_modifiers(edge); 314 return field_infos->store(osfi); 315 } 316 317 typedef JfrArtifactWriterImplHost<const FieldTable::FieldInfoEntry*, __write_field_info__> FieldWriterImpl; 318 typedef JfrArtifactWriterHost<FieldWriterImpl, CONSTANT_TYPE_OLDOBJECTFIELD> FieldWriter; 319 320 static void write_field_infos(JfrCheckpointWriter& writer) { 321 if (field_infos != NULL) { 322 FieldWriter fw(&writer, NULL, false); 323 field_infos->iterate(fw); 324 } 325 } 326 327 static const char* description(const ObjectSampleRootDescriptionInfo* osdi) { 328 assert(osdi != NULL, "invariant"); 329 330 if (osdi->_data._description == NULL) { 331 return NULL; 332 } 333 334 ObjectDescriptionBuilder description; 335 if (osdi->_data._system == OldObjectRoot::_threads) { 336 description.write_text("Thread Name: "); 337 } 338 description.write_text(osdi->_data._description); 339 return description.description(); 340 } 341 342 int __write_root_description_info__(JfrCheckpointWriter* writer, JfrArtifactSet* unused, const void* di) { 343 assert(writer != NULL, "invariant"); 344 assert(di != NULL, "invariant"); 345 const ObjectSampleRootDescriptionInfo* const osdi = (const ObjectSampleRootDescriptionInfo*)di; 346 writer->write(osdi->_id); 347 writer->write(description(osdi)); 348 writer->write<u8>(osdi->_data._system); 349 writer->write<u8>(osdi->_data._type); 350 return 1; 351 } 352 353 static traceid get_root_description_info_id(const Edge& edge, traceid id) { 354 assert(edge.is_root(), "invariant"); 355 if (EdgeUtils::is_leak_edge(edge)) { 356 return 0; 357 } 358 359 if (root_infos == NULL) { 360 root_infos = new RootDescriptionInfo(); 361 } 362 assert(root_infos != NULL, "invariant"); 363 ObjectSampleRootDescriptionInfo* const oodi = new ObjectSampleRootDescriptionInfo(); 364 oodi->_id = id; 365 oodi->_data._root_edge = &edge; 366 return root_infos->store(oodi); 367 } 368 369 typedef JfrArtifactWriterImplHost<const ObjectSampleRootDescriptionInfo*, __write_root_description_info__> RootDescriptionWriterImpl; 370 typedef JfrArtifactWriterHost<RootDescriptionWriterImpl, CONSTANT_TYPE_OLDOBJECTGCROOT> RootDescriptionWriter; 371 372 373 int _edge_reference_compare_(uintptr_t lhs, uintptr_t rhs) { 374 return lhs > rhs ? 1 : (lhs < rhs) ? -1 : 0; 375 } 376 377 int _root_desc_compare_(const ObjectSampleRootDescriptionInfo*const & lhs, const ObjectSampleRootDescriptionInfo* const& rhs) { 378 const uintptr_t lhs_ref = (uintptr_t)lhs->_data._root_edge->reference(); 379 const uintptr_t rhs_ref = (uintptr_t)rhs->_data._root_edge->reference(); 380 return _edge_reference_compare_(lhs_ref, rhs_ref); 381 } 382 383 static int find_sorted(const RootCallbackInfo& callback_info, 384 const GrowableArray<const ObjectSampleRootDescriptionInfo*>* arr, 385 int length, 386 bool& found) { 387 assert(arr != NULL, "invariant"); 388 assert(length >= 0, "invariant"); 389 assert(length <= arr->length(), "invariant"); 390 391 found = false; 392 int min = 0; 393 int max = length; 394 while (max >= min) { 395 const int mid = (int)(((uint)max + min) / 2); 396 int diff = _edge_reference_compare_((uintptr_t)callback_info._high, 397 (uintptr_t)arr->at(mid)->_data._root_edge->reference()); 398 if (diff > 0) { 399 min = mid + 1; 400 } else if (diff < 0) { 401 max = mid - 1; 402 } else { 403 found = true; 404 return mid; 405 } 406 } 407 return min; 408 } 409 410 class RootResolutionSet : public ResourceObj, public RootCallback { 411 private: 412 GrowableArray<const ObjectSampleRootDescriptionInfo*>* _unresolved_roots; 413 414 const uintptr_t high() const { 415 return (uintptr_t)_unresolved_roots->last()->_data._root_edge->reference(); 416 } 417 418 const uintptr_t low() const { 419 return (uintptr_t)_unresolved_roots->first()->_data._root_edge->reference(); 420 } 421 422 bool in_set_address_range(const RootCallbackInfo& callback_info) const { 423 assert(callback_info._low == NULL, "invariant"); 424 const uintptr_t addr = (uintptr_t)callback_info._high; 425 return low() <= addr && high() >= addr; 426 } 427 428 int compare_to_range(const RootCallbackInfo& callback_info) const { 429 assert(callback_info._high != NULL, "invariant"); 430 assert(callback_info._low != NULL, "invariant"); 431 432 for (int i = 0; i < _unresolved_roots->length(); ++i) { 433 const uintptr_t ref_addr = (uintptr_t)_unresolved_roots->at(i)->_data._root_edge->reference(); 434 if ((uintptr_t)callback_info._low <= ref_addr && (uintptr_t)callback_info._high >= ref_addr) { 435 return i; 436 } 437 } 438 return -1; 439 } 440 441 int exact(const RootCallbackInfo& callback_info) const { 442 assert(callback_info._high != NULL, "invariant"); 443 assert(in_set_address_range(callback_info), "invariant"); 444 445 bool found; 446 const int idx = find_sorted(callback_info, _unresolved_roots, _unresolved_roots->length(), found); 447 return found ? idx : -1; 448 } 449 450 bool resolve_root(const RootCallbackInfo& callback_info, int idx) const { 451 assert(idx >= 0, "invariant"); 452 assert(idx < _unresolved_roots->length(), "invariant"); 453 454 ObjectSampleRootDescriptionInfo* const desc = 455 const_cast<ObjectSampleRootDescriptionInfo*>(_unresolved_roots->at(idx)); 456 assert(desc != NULL, "invariant"); 457 assert((uintptr_t)callback_info._high == (uintptr_t)desc->_data._root_edge->reference(), "invariant"); 458 459 desc->_data._system = callback_info._system; 460 desc->_data._type = callback_info._type; 461 462 if (callback_info._system == OldObjectRoot::_threads) { 463 const JavaThread* jt = (const JavaThread*)callback_info._context; 464 assert(jt != NULL, "invariant"); 465 desc->_data._description = jt->name(); 466 } 467 468 _unresolved_roots->remove_at(idx); 469 return _unresolved_roots->is_empty(); 470 } 471 472 public: 473 RootResolutionSet(RootDescriptionInfo* info) : _unresolved_roots(NULL) { 474 assert(info != NULL, "invariant"); 475 // construct a sorted copy 476 const GrowableArray<const ObjectSampleRootDescriptionInfo*>& info_storage = info->storage(); 477 const int length = info_storage.length(); 478 _unresolved_roots = new GrowableArray<const ObjectSampleRootDescriptionInfo*>(length); 479 assert(_unresolved_roots != NULL, "invariant"); 480 481 for (int i = 0; i < length; ++i) { 482 _unresolved_roots->insert_sorted<_root_desc_compare_>(info_storage.at(i)); 483 } 484 } 485 486 bool process(const RootCallbackInfo& callback_info) { 487 if (NULL == callback_info._low) { 488 if (in_set_address_range(callback_info)) { 489 const int idx = exact(callback_info); 490 return idx == -1 ? false : resolve_root(callback_info, idx); 491 } 492 return false; 493 } 494 assert(callback_info._low != NULL, "invariant"); 495 const int idx = compare_to_range(callback_info); 496 return idx == -1 ? false : resolve_root(callback_info, idx); 497 } 498 499 int entries() const { 500 return _unresolved_roots->length(); 501 } 502 503 const void* at(int idx) const { 504 assert(idx >= 0, "invariant"); 505 assert(idx < _unresolved_roots->length(), "invariant"); 506 return _unresolved_roots->at(idx)->_data._root_edge->reference(); 507 } 508 }; 509 510 static void write_root_descriptors(JfrCheckpointWriter& writer) { 511 if (root_infos != NULL) { 512 // resolve roots 513 RootResolutionSet rrs(root_infos); 514 RootResolver::resolve(rrs); 515 // write roots 516 RootDescriptionWriter rw(&writer, NULL, false); 517 root_infos->iterate(rw); 518 } 519 } 520 521 static void add_old_object_sample_info(const Edge* current, traceid id) { 522 assert(current != NULL, "invariant"); 523 if (sample_infos == NULL) { 524 sample_infos = new SampleInfo(); 525 } 526 assert(sample_infos != NULL, "invariant"); 527 OldObjectSampleInfo* const oosi = new OldObjectSampleInfo(); 528 assert(oosi != NULL, "invariant"); 529 oosi->_id = id; 530 oosi->_data._object = current->pointee(); 531 oosi->_data._reference_id = current->is_root() ? (traceid)0 : id; 532 sample_infos->store(oosi); 533 } 534 535 static void add_reference_info(const RoutableEdge* current, traceid id, traceid parent_id) { 536 assert(current != NULL, "invariant"); 537 if (ref_infos == NULL) { 538 ref_infos = new RefInfo(); 539 } 540 541 assert(ref_infos != NULL, "invariant"); 542 ReferenceInfo* const ri = new ReferenceInfo(); 543 assert(ri != NULL, "invariant"); 544 545 ri->_id = id; 546 ri->_data._array_info_id = !current->is_skip_edge() ? get_array_info_id(*current, id) : 0; 547 ri->_data._field_info_id = ri->_data._array_info_id == 0 && !current->is_skip_edge() ? 548 get_field_info_id(*current) : (traceid)0; 549 ri->_data._old_object_sample_id = parent_id; 550 ri->_data._skip = current->skip_length(); 551 ref_infos->store(ri); 552 } 553 554 static traceid add_root_info(const Edge* root, traceid id) { 555 assert(root != NULL, "invariant"); 556 assert(root->is_root(), "invariant"); 557 return get_root_description_info_id(*root, id); 558 } 559 560 void ObjectSampleWriter::write(const RoutableEdge* edge) { 561 assert(edge != NULL, "invariant"); 562 const traceid id = _store->get_id(edge); 563 add_old_object_sample_info(edge, id); 564 const RoutableEdge* parent = edge->logical_parent(); 565 if (parent != NULL) { 566 add_reference_info(edge, id, _store->get_id(parent)); 567 } else { 568 assert(edge->is_root(), "invariant"); 569 add_root_info(edge, id); 570 } 571 } 572 573 ObjectSampleWriter::ObjectSampleWriter(JfrCheckpointWriter& writer, const EdgeStore* store) : 574 _writer(writer), 575 _store(store) { 576 assert(store != NULL, "invariant"); 577 assert(store->number_of_entries() > 0, "invariant"); 578 sample_infos = NULL; 579 ref_infos = NULL; 580 array_infos = NULL; 581 field_infos = NULL; 582 root_infos = NULL; 583 } 584 585 ObjectSampleWriter::~ObjectSampleWriter() { 586 write_sample_infos(_writer); 587 write_reference_infos(_writer); 588 write_array_infos(_writer); 589 write_field_infos(_writer); 590 write_root_descriptors(_writer); 591 } 592 593 void ObjectSampleWriter::write_chain(const RoutableEdge& edge) { 594 assert(EdgeUtils::is_leak_edge(edge), "invariant"); 595 if (edge.processed()) { 596 return; 597 } 598 EdgeUtils::collapse_chain(edge); 599 const RoutableEdge* current = &edge; 600 while (current != NULL) { 601 if (current->processed()) { 602 return; 603 } 604 write(current); 605 current->set_processed(); 606 current = current->logical_parent(); 607 } 608 } 609 610 bool ObjectSampleWriter::operator()(const RoutableEdge& edge) { 611 if (EdgeUtils::is_leak_edge(edge)) { 612 write_chain(edge); 613 } 614 return true; 615 }