Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. | 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
| 3 * | 3 * |
| 4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
| 5 * modification, are permitted provided that the following conditions | 5 * modification, are permitted provided that the following conditions |
| 6 * are met: | 6 * are met: |
| 7 * 1. Redistributions of source code must retain the above copyright | 7 * 1. Redistributions of source code must retain the above copyright |
| 8 * notice, this list of conditions and the following disclaimer. | 8 * notice, this list of conditions and the following disclaimer. |
| 9 * 2. Redistributions in binary form must reproduce the above copyright | 9 * 2. Redistributions in binary form must reproduce the above copyright |
| 10 * notice, this list of conditions and the following disclaimer in the | 10 * notice, this list of conditions and the following disclaimer in the |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 24 * | 24 * |
| 25 */ | 25 */ |
| 26 | 26 |
| 27 #include "core/workers/WorkerThread.h" | 27 #include "core/workers/WorkerThread.h" |
| 28 | 28 |
| 29 #include <limits.h> | 29 #include <limits.h> |
| 30 #include <memory> | 30 #include <memory> |
| 31 #include "bindings/core/v8/Microtask.h" | 31 #include "bindings/core/v8/Microtask.h" |
| 32 #include "bindings/core/v8/ScriptSourceCode.h" | 32 #include "bindings/core/v8/ScriptSourceCode.h" |
| 33 #include "bindings/core/v8/WorkerOrWorkletScriptController.h" | 33 #include "bindings/core/v8/WorkerOrWorkletScriptController.h" |
| 34 #include "core/dom/TaskRunnerHelper.h" | |
| 34 #include "core/inspector/ConsoleMessageStorage.h" | 35 #include "core/inspector/ConsoleMessageStorage.h" |
| 35 #include "core/inspector/InspectorTaskRunner.h" | 36 #include "core/inspector/InspectorTaskRunner.h" |
| 36 #include "core/inspector/WorkerInspectorController.h" | 37 #include "core/inspector/WorkerInspectorController.h" |
| 37 #include "core/inspector/WorkerThreadDebugger.h" | 38 #include "core/inspector/WorkerThreadDebugger.h" |
| 38 #include "core/origin_trials/OriginTrialContext.h" | 39 #include "core/origin_trials/OriginTrialContext.h" |
| 39 #include "core/probe/CoreProbes.h" | 40 #include "core/probe/CoreProbes.h" |
| 40 #include "core/workers/ThreadedWorkletGlobalScope.h" | 41 #include "core/workers/ThreadedWorkletGlobalScope.h" |
| 41 #include "core/workers/WorkerBackingThread.h" | 42 #include "core/workers/WorkerBackingThread.h" |
| 42 #include "core/workers/WorkerClients.h" | 43 #include "core/workers/WorkerClients.h" |
| 43 #include "core/workers/WorkerGlobalScope.h" | 44 #include "core/workers/WorkerGlobalScope.h" |
| 44 #include "core/workers/WorkerReportingProxy.h" | 45 #include "core/workers/WorkerReportingProxy.h" |
| 45 #include "core/workers/WorkerThreadStartupData.h" | 46 #include "core/workers/WorkerThreadStartupData.h" |
| 46 #include "platform/CrossThreadFunctional.h" | 47 #include "platform/CrossThreadFunctional.h" |
| 47 #include "platform/Histogram.h" | 48 #include "platform/Histogram.h" |
| 48 #include "platform/WaitableEvent.h" | 49 #include "platform/WaitableEvent.h" |
| 49 #include "platform/WebThreadSupportingGC.h" | 50 #include "platform/WebThreadSupportingGC.h" |
| 50 #include "platform/heap/SafePoint.h" | 51 #include "platform/heap/SafePoint.h" |
| 51 #include "platform/heap/ThreadState.h" | 52 #include "platform/heap/ThreadState.h" |
| 52 #include "platform/weborigin/KURL.h" | 53 #include "platform/weborigin/KURL.h" |
| 54 #include "public/platform/scheduler/child/webthread_impl_for_worker_scheduler.h" | |
| 53 #include "wtf/Functional.h" | 55 #include "wtf/Functional.h" |
| 54 #include "wtf/Noncopyable.h" | 56 #include "wtf/Noncopyable.h" |
| 55 #include "wtf/PtrUtil.h" | 57 #include "wtf/PtrUtil.h" |
| 56 #include "wtf/Threading.h" | 58 #include "wtf/Threading.h" |
| 57 #include "wtf/text/WTFString.h" | 59 #include "wtf/text/WTFString.h" |
| 58 | 60 |
| 59 namespace blink { | 61 namespace blink { |
| 60 | 62 |
| 61 using ExitCode = WorkerThread::ExitCode; | 63 using ExitCode = WorkerThread::ExitCode; |
| 62 | 64 |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 100 DEFINE_THREAD_SAFE_STATIC_LOCAL( | 102 DEFINE_THREAD_SAFE_STATIC_LOCAL( |
| 101 EnumerationHistogram, exitCodeHistogram, | 103 EnumerationHistogram, exitCodeHistogram, |
| 102 new EnumerationHistogram("WorkerThread.ExitCode", | 104 new EnumerationHistogram("WorkerThread.ExitCode", |
| 103 static_cast<int>(ExitCode::LastEnum))); | 105 static_cast<int>(ExitCode::LastEnum))); |
| 104 exitCodeHistogram.count(static_cast<int>(m_exitCode)); | 106 exitCodeHistogram.count(static_cast<int>(m_exitCode)); |
| 105 } | 107 } |
| 106 | 108 |
| 107 void WorkerThread::start(std::unique_ptr<WorkerThreadStartupData> startupData, | 109 void WorkerThread::start(std::unique_ptr<WorkerThreadStartupData> startupData, |
| 108 ParentFrameTaskRunners* parentFrameTaskRunners) { | 110 ParentFrameTaskRunners* parentFrameTaskRunners) { |
| 109 DCHECK(isMainThread()); | 111 DCHECK(isMainThread()); |
| 110 | |
| 111 if (m_requestedToStart) | 112 if (m_requestedToStart) |
| 112 return; | 113 return; |
| 113 | 114 |
| 114 m_requestedToStart = true; | 115 m_requestedToStart = true; |
| 115 m_parentFrameTaskRunners = parentFrameTaskRunners; | 116 m_parentFrameTaskRunners = parentFrameTaskRunners; |
| 116 workerBackingThread().backingThread().postTask( | 117 m_threadControlTaskRunner = |
| 118 workerBackingThread().backingThread().platformThread().getWebTaskRunner(); | |
| 119 | |
| 120 // Synchronously initialize the per-global-scope scheduler to prevent someone | |
| 121 // from posting a task to the thread before the scheduler is ready. | |
| 122 WaitableEvent waitableEvent; | |
| 123 m_threadControlTaskRunner->postTask( | |
| 124 BLINK_FROM_HERE, | |
| 125 crossThreadBind(&WorkerThread::initializeSchedulerOnWorkerThread, | |
| 126 crossThreadUnretained(this), | |
| 127 crossThreadUnretained(&waitableEvent))); | |
|
kinuko
2017/04/10 06:30:06
Is it the requirement of NewTaskQueue implementati
Sami
2017/04/10 16:58:10
Yes, that's the case.
| |
| 128 waitableEvent.wait(); | |
| 129 | |
| 130 m_threadControlTaskRunner->postTask( | |
| 117 BLINK_FROM_HERE, crossThreadBind(&WorkerThread::initializeOnWorkerThread, | 131 BLINK_FROM_HERE, crossThreadBind(&WorkerThread::initializeOnWorkerThread, |
| 118 crossThreadUnretained(this), | 132 crossThreadUnretained(this), |
| 119 WTF::passed(std::move(startupData)))); | 133 WTF::passed(std::move(startupData)))); |
| 120 } | 134 } |
| 121 | 135 |
| 122 void WorkerThread::terminate() { | 136 void WorkerThread::terminate() { |
| 123 DCHECK(isMainThread()); | 137 DCHECK(isMainThread()); |
| 124 terminateInternal(TerminationMode::Graceful); | 138 terminateInternal(TerminationMode::Graceful); |
| 125 } | 139 } |
| 126 | 140 |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 183 } | 197 } |
| 184 | 198 |
| 185 v8::Isolate* WorkerThread::isolate() { | 199 v8::Isolate* WorkerThread::isolate() { |
| 186 return workerBackingThread().isolate(); | 200 return workerBackingThread().isolate(); |
| 187 } | 201 } |
| 188 | 202 |
| 189 bool WorkerThread::isCurrentThread() { | 203 bool WorkerThread::isCurrentThread() { |
| 190 return workerBackingThread().backingThread().isCurrentThread(); | 204 return workerBackingThread().backingThread().isCurrentThread(); |
| 191 } | 205 } |
| 192 | 206 |
| 193 void WorkerThread::postTask(const WebTraceLocation& location, | |
| 194 std::unique_ptr<WTF::Closure> task) { | |
| 195 DCHECK(isCurrentThread()); | |
| 196 if (isInShutdown()) | |
| 197 return; | |
| 198 workerBackingThread().backingThread().postTask( | |
| 199 location, | |
| 200 WTF::bind( | |
| 201 &WorkerThread::performTaskOnWorkerThread<WTF::SameThreadAffinity>, | |
| 202 WTF::unretained(this), WTF::passed(std::move(task)))); | |
| 203 } | |
| 204 | |
| 205 void WorkerThread::postTask(const WebTraceLocation& location, | |
| 206 std::unique_ptr<WTF::CrossThreadClosure> task) { | |
| 207 if (isInShutdown()) | |
| 208 return; | |
| 209 workerBackingThread().backingThread().postTask( | |
| 210 location, | |
| 211 crossThreadBind( | |
| 212 &WorkerThread::performTaskOnWorkerThread<WTF::CrossThreadAffinity>, | |
| 213 crossThreadUnretained(this), WTF::passed(std::move(task)))); | |
| 214 } | |
| 215 | |
| 216 void WorkerThread::appendDebuggerTask( | 207 void WorkerThread::appendDebuggerTask( |
| 217 std::unique_ptr<CrossThreadClosure> task) { | 208 std::unique_ptr<CrossThreadClosure> task) { |
| 218 DCHECK(isMainThread()); | 209 DCHECK(isMainThread()); |
| 219 if (isInShutdown()) | 210 if (m_requestedToTerminate) |
| 220 return; | 211 return; |
| 221 m_inspectorTaskRunner->appendTask(crossThreadBind( | 212 m_inspectorTaskRunner->appendTask(crossThreadBind( |
| 222 &WorkerThread::performDebuggerTaskOnWorkerThread, | 213 &WorkerThread::performDebuggerTaskOnWorkerThread, |
| 223 crossThreadUnretained(this), WTF::passed(std::move(task)))); | 214 crossThreadUnretained(this), WTF::passed(std::move(task)))); |
| 224 { | 215 { |
| 225 MutexLocker lock(m_threadStateMutex); | 216 MutexLocker lock(m_threadStateMutex); |
| 226 if (isolate() && m_threadState != ThreadState::ReadyToShutdown) | 217 if (isolate() && m_threadState != ThreadState::ReadyToShutdown) |
| 227 m_inspectorTaskRunner->interruptAndRunAllTasksDontWait(isolate()); | 218 m_inspectorTaskRunner->interruptAndRunAllTasksDontWait(isolate()); |
| 228 } | 219 } |
| 229 workerBackingThread().backingThread().postTask( | 220 TaskRunnerHelper::get(TaskType::Unthrottled, this) |
| 230 BLINK_FROM_HERE, | 221 ->postTask(BLINK_FROM_HERE, |
| 231 crossThreadBind(&WorkerThread::performDebuggerTaskDontWaitOnWorkerThread, | 222 crossThreadBind( |
| 232 crossThreadUnretained(this))); | 223 &WorkerThread::performDebuggerTaskDontWaitOnWorkerThread, |
| 224 crossThreadUnretained(this))); | |
| 233 } | 225 } |
| 234 | 226 |
| 235 void WorkerThread::startRunningDebuggerTasksOnPauseOnWorkerThread() { | 227 void WorkerThread::startRunningDebuggerTasksOnPauseOnWorkerThread() { |
| 236 DCHECK(isCurrentThread()); | 228 DCHECK(isCurrentThread()); |
| 237 if (m_workerInspectorController) | 229 if (m_workerInspectorController) |
| 238 m_workerInspectorController->flushProtocolNotifications(); | 230 m_workerInspectorController->flushProtocolNotifications(); |
| 239 m_pausedInDebugger = true; | 231 m_pausedInDebugger = true; |
| 240 ThreadDebugger::idleStarted(isolate()); | 232 ThreadDebugger::idleStarted(isolate()); |
| 241 std::unique_ptr<CrossThreadClosure> task; | 233 std::unique_ptr<CrossThreadClosure> task; |
| 242 do { | 234 do { |
| (...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 361 WTF::unretained(this)), | 353 WTF::unretained(this)), |
| 362 m_forcibleTerminationDelayInMs); | 354 m_forcibleTerminationDelayInMs); |
| 363 break; | 355 break; |
| 364 } | 356 } |
| 365 } | 357 } |
| 366 } | 358 } |
| 367 | 359 |
| 368 m_workerThreadLifecycleContext->notifyContextDestroyed(); | 360 m_workerThreadLifecycleContext->notifyContextDestroyed(); |
| 369 m_inspectorTaskRunner->kill(); | 361 m_inspectorTaskRunner->kill(); |
| 370 | 362 |
| 371 workerBackingThread().backingThread().postTask( | 363 m_threadControlTaskRunner->postTask( |
| 372 BLINK_FROM_HERE, | 364 BLINK_FROM_HERE, |
| 373 crossThreadBind(&WorkerThread::prepareForShutdownOnWorkerThread, | 365 crossThreadBind(&WorkerThread::prepareForShutdownOnWorkerThread, |
| 374 crossThreadUnretained(this))); | 366 crossThreadUnretained(this))); |
| 375 workerBackingThread().backingThread().postTask( | 367 m_threadControlTaskRunner->postTask( |
| 376 BLINK_FROM_HERE, | 368 BLINK_FROM_HERE, |
| 377 crossThreadBind(&WorkerThread::performShutdownOnWorkerThread, | 369 crossThreadBind(&WorkerThread::performShutdownOnWorkerThread, |
| 378 crossThreadUnretained(this))); | 370 crossThreadUnretained(this))); |
| 379 } | 371 } |
| 380 | 372 |
| 381 bool WorkerThread::shouldScheduleToTerminateExecution(const MutexLocker& lock) { | 373 bool WorkerThread::shouldScheduleToTerminateExecution(const MutexLocker& lock) { |
| 382 DCHECK(isMainThread()); | 374 DCHECK(isMainThread()); |
| 383 DCHECK(isThreadStateMutexLocked(lock)); | 375 DCHECK(isThreadStateMutexLocked(lock)); |
| 384 | 376 |
| 385 switch (m_threadState) { | 377 switch (m_threadState) { |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 423 DCHECK(isThreadStateMutexLocked(lock)); | 415 DCHECK(isThreadStateMutexLocked(lock)); |
| 424 | 416 |
| 425 DCHECK(exitCode == ExitCode::SyncForciblyTerminated || | 417 DCHECK(exitCode == ExitCode::SyncForciblyTerminated || |
| 426 exitCode == ExitCode::AsyncForciblyTerminated); | 418 exitCode == ExitCode::AsyncForciblyTerminated); |
| 427 setExitCode(lock, exitCode); | 419 setExitCode(lock, exitCode); |
| 428 | 420 |
| 429 isolate()->TerminateExecution(); | 421 isolate()->TerminateExecution(); |
| 430 m_forcibleTerminationTaskHandle.cancel(); | 422 m_forcibleTerminationTaskHandle.cancel(); |
| 431 } | 423 } |
| 432 | 424 |
| 433 bool WorkerThread::isInShutdown() { | 425 void WorkerThread::initializeSchedulerOnWorkerThread( |
| 434 // Check if we've started termination or shutdown sequence. Avoid acquiring | 426 WaitableEvent* waitableEvent) { |
| 435 // a lock here to avoid introducing a risk of deadlock. Note that accessing | 427 DCHECK(isCurrentThread()); |
| 436 // |m_requestedToTerminate| on the main thread or |m_threadState| on the | 428 scheduler::WebThreadImplForWorkerScheduler& workerScheduler = |
| 437 // worker thread is safe as the flag is set only on the thread. | 429 static_cast<scheduler::WebThreadImplForWorkerScheduler&>( |
| 438 if (isMainThread() && m_requestedToTerminate) | 430 workerBackingThread().backingThread().platformThread()); |
| 439 return true; | 431 m_globalScopeScheduler = WTF::makeUnique<scheduler::GlobalScopeScheduler>( |
| 440 if (isCurrentThread() && m_threadState == ThreadState::ReadyToShutdown) | 432 workerScheduler.getWorkerScheduler()); |
| 441 return true; | 433 waitableEvent->signal(); |
| 442 return false; | |
| 443 } | 434 } |
| 444 | 435 |
| 445 void WorkerThread::initializeOnWorkerThread( | 436 void WorkerThread::initializeOnWorkerThread( |
| 446 std::unique_ptr<WorkerThreadStartupData> startupData) { | 437 std::unique_ptr<WorkerThreadStartupData> startupData) { |
| 447 DCHECK(isCurrentThread()); | 438 DCHECK(isCurrentThread()); |
| 448 DCHECK_EQ(ThreadState::NotStarted, m_threadState); | 439 DCHECK_EQ(ThreadState::NotStarted, m_threadState); |
| 449 | 440 |
| 450 KURL scriptURL = startupData->m_scriptURL; | 441 KURL scriptURL = startupData->m_scriptURL; |
| 451 String sourceCode = startupData->m_sourceCode; | 442 String sourceCode = startupData->m_sourceCode; |
| 452 WorkerThreadStartMode startMode = startupData->m_startMode; | 443 WorkerThreadStartMode startMode = startupData->m_startMode; |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 530 probe::allAsyncTasksCanceled(globalScope()); | 521 probe::allAsyncTasksCanceled(globalScope()); |
| 531 | 522 |
| 532 globalScope()->notifyContextDestroyed(); | 523 globalScope()->notifyContextDestroyed(); |
| 533 if (m_workerInspectorController) { | 524 if (m_workerInspectorController) { |
| 534 m_workerInspectorController->dispose(); | 525 m_workerInspectorController->dispose(); |
| 535 m_workerInspectorController.clear(); | 526 m_workerInspectorController.clear(); |
| 536 } | 527 } |
| 537 globalScope()->dispose(); | 528 globalScope()->dispose(); |
| 538 m_consoleMessageStorage.clear(); | 529 m_consoleMessageStorage.clear(); |
| 539 workerBackingThread().backingThread().removeTaskObserver(this); | 530 workerBackingThread().backingThread().removeTaskObserver(this); |
| 531 | |
| 532 // Cancel all queued tasks. | |
| 533 m_globalScopeScheduler->dispose(); | |
| 540 } | 534 } |
| 541 | 535 |
| 542 void WorkerThread::performShutdownOnWorkerThread() { | 536 void WorkerThread::performShutdownOnWorkerThread() { |
| 543 DCHECK(isCurrentThread()); | 537 DCHECK(isCurrentThread()); |
| 544 DCHECK(checkRequestedToTerminateOnWorkerThread()); | 538 DCHECK(checkRequestedToTerminateOnWorkerThread()); |
| 545 DCHECK_EQ(ThreadState::ReadyToShutdown, m_threadState); | 539 DCHECK_EQ(ThreadState::ReadyToShutdown, m_threadState); |
| 546 | 540 |
| 547 // The below assignment will destroy the context, which will in turn notify | 541 // The below assignment will destroy the context, which will in turn notify |
| 548 // messaging proxy. We cannot let any objects survive past thread exit, | 542 // messaging proxy. We cannot let any objects survive past thread exit, |
| 549 // because no other thread will run GC or otherwise destroy them. If Oilpan | 543 // because no other thread will run GC or otherwise destroy them. If Oilpan |
| 550 // is enabled, we detach of the context/global scope, with the final heap | 544 // is enabled, we detach of the context/global scope, with the final heap |
| 551 // cleanup below sweeping it out. | 545 // cleanup below sweeping it out. |
| 552 m_globalScope = nullptr; | 546 m_globalScope = nullptr; |
| 553 | 547 |
| 554 if (isOwningBackingThread()) | 548 if (isOwningBackingThread()) |
| 555 workerBackingThread().shutdown(); | 549 workerBackingThread().shutdown(); |
| 556 // We must not touch workerBackingThread() from now on. | 550 // We must not touch workerBackingThread() from now on. |
| 557 | 551 |
| 558 // Notify the proxy that the WorkerOrWorkletGlobalScope has been disposed | 552 // Notify the proxy that the WorkerOrWorkletGlobalScope has been disposed |
| 559 // of. This can free this thread object, hence it must not be touched | 553 // of. This can free this thread object, hence it must not be touched |
| 560 // afterwards. | 554 // afterwards. |
| 561 workerReportingProxy().didTerminateWorkerThread(); | 555 workerReportingProxy().didTerminateWorkerThread(); |
| 562 | 556 |
| 563 m_shutdownEvent->signal(); | 557 m_shutdownEvent->signal(); |
| 564 } | 558 } |
| 565 | 559 |
| 566 template <WTF::FunctionThreadAffinity threadAffinity> | |
| 567 void WorkerThread::performTaskOnWorkerThread( | |
| 568 std::unique_ptr<Function<void(), threadAffinity>> task) { | |
| 569 DCHECK(isCurrentThread()); | |
| 570 if (m_threadState != ThreadState::Running) | |
| 571 return; | |
| 572 | |
| 573 { | |
| 574 DEFINE_THREAD_SAFE_STATIC_LOCAL( | |
| 575 CustomCountHistogram, scopedUsCounter, | |
| 576 new CustomCountHistogram("WorkerThread.Task.Time", 0, 10000000, 50)); | |
| 577 ScopedUsHistogramTimer timer(scopedUsCounter); | |
| 578 (*task)(); | |
| 579 } | |
| 580 } | |
| 581 | |
| 582 void WorkerThread::performDebuggerTaskOnWorkerThread( | 560 void WorkerThread::performDebuggerTaskOnWorkerThread( |
| 583 std::unique_ptr<CrossThreadClosure> task) { | 561 std::unique_ptr<CrossThreadClosure> task) { |
| 584 DCHECK(isCurrentThread()); | 562 DCHECK(isCurrentThread()); |
| 585 InspectorTaskRunner::IgnoreInterruptsScope scope(m_inspectorTaskRunner.get()); | 563 InspectorTaskRunner::IgnoreInterruptsScope scope(m_inspectorTaskRunner.get()); |
| 586 { | 564 { |
| 587 MutexLocker lock(m_threadStateMutex); | 565 MutexLocker lock(m_threadStateMutex); |
| 588 DCHECK_EQ(ThreadState::Running, m_threadState); | 566 DCHECK_EQ(ThreadState::Running, m_threadState); |
| 589 m_runningDebuggerTask = true; | 567 m_runningDebuggerTask = true; |
| 590 } | 568 } |
| 591 ThreadDebugger::idleFinished(isolate()); | 569 ThreadDebugger::idleFinished(isolate()); |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 656 MutexLocker lock(m_threadStateMutex); | 634 MutexLocker lock(m_threadStateMutex); |
| 657 return m_requestedToTerminate; | 635 return m_requestedToTerminate; |
| 658 } | 636 } |
| 659 | 637 |
| 660 ExitCode WorkerThread::getExitCodeForTesting() { | 638 ExitCode WorkerThread::getExitCodeForTesting() { |
| 661 MutexLocker lock(m_threadStateMutex); | 639 MutexLocker lock(m_threadStateMutex); |
| 662 return m_exitCode; | 640 return m_exitCode; |
| 663 } | 641 } |
| 664 | 642 |
| 665 } // namespace blink | 643 } // namespace blink |
| OLD | NEW |