|
|
@@ -18,22 +18,9 @@ Documentation for GNU make jobserver
|
|
|
|
|
|
http://make.mad-scientist.net/papers/jobserver-implementation/
|
|
|
|
|
|
-Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
----
|
|
|
- configure.py | 2 +
|
|
|
- src/build.cc | 63 ++++++++----
|
|
|
- src/build.h | 3 +
|
|
|
- src/tokenpool-gnu-make.cc | 211 ++++++++++++++++++++++++++++++++++++++
|
|
|
- src/tokenpool-none.cc | 27 +++++
|
|
|
- src/tokenpool.h | 26 +++++
|
|
|
- 6 files changed, 310 insertions(+), 22 deletions(-)
|
|
|
- create mode 100644 src/tokenpool-gnu-make.cc
|
|
|
- create mode 100644 src/tokenpool-none.cc
|
|
|
- create mode 100644 src/tokenpool.h
|
|
|
-
|
|
|
--- a/configure.py
|
|
|
+++ b/configure.py
|
|
|
-@@ -519,11 +519,13 @@ for name in ['build',
|
|
|
+@@ -543,11 +543,13 @@ for name in ['build',
|
|
|
'state',
|
|
|
'status',
|
|
|
'string_piece_util',
|
|
|
@@ -47,7 +34,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
'includes_normalize-win32',
|
|
|
'msvc_helper-win32',
|
|
|
'msvc_helper_main-win32']:
|
|
|
-@@ -532,7 +534,9 @@ if platform.is_windows():
|
|
|
+@@ -556,7 +558,9 @@ if platform.is_windows():
|
|
|
objs += cxx('minidump-win32', variables=cxxvariables)
|
|
|
objs += cc('getopt')
|
|
|
else:
|
|
|
@@ -58,17 +45,17 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
if platform.is_aix():
|
|
|
objs += cc('getopt')
|
|
|
if platform.is_msvc():
|
|
|
-@@ -590,6 +594,7 @@ for name in ['build_log_test',
|
|
|
- 'string_piece_util_test',
|
|
|
- 'subprocess_test',
|
|
|
- 'test',
|
|
|
-+ 'tokenpool_test',
|
|
|
- 'util_test']:
|
|
|
- objs += cxx(name, variables=cxxvariables)
|
|
|
- if platform.is_windows():
|
|
|
+@@ -639,6 +643,7 @@ if gtest_src_dir:
|
|
|
+ 'string_piece_util_test',
|
|
|
+ 'subprocess_test',
|
|
|
+ 'test',
|
|
|
++ 'tokenpool_test',
|
|
|
+ 'util_test',
|
|
|
+ ]
|
|
|
+ if platform.is_windows():
|
|
|
--- a/src/build.cc
|
|
|
+++ b/src/build.cc
|
|
|
-@@ -35,6 +35,7 @@
|
|
|
+@@ -39,6 +39,7 @@
|
|
|
#include "state.h"
|
|
|
#include "status.h"
|
|
|
#include "subprocess.h"
|
|
|
@@ -76,10 +63,12 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
#include "util.h"
|
|
|
|
|
|
using namespace std;
|
|
|
-@@ -47,8 +48,9 @@ struct DryRunCommandRunner : public Comm
|
|
|
+@@ -50,24 +51,29 @@ struct DryRunCommandRunner : public Comm
|
|
|
+ virtual ~DryRunCommandRunner() {}
|
|
|
|
|
|
// Overridden from CommandRunner:
|
|
|
- virtual bool CanRunMore() const;
|
|
|
+- virtual size_t CanRunMore() const;
|
|
|
++ virtual size_t CanRunMore();
|
|
|
+ virtual bool AcquireToken();
|
|
|
virtual bool StartCommand(Edge* edge);
|
|
|
- virtual bool WaitForCommand(Result* result);
|
|
|
@@ -87,8 +76,11 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
|
|
|
private:
|
|
|
queue<Edge*> finished_;
|
|
|
-@@ -58,12 +60,16 @@ bool DryRunCommandRunner::CanRunMore() c
|
|
|
- return true;
|
|
|
+ };
|
|
|
+
|
|
|
+-size_t DryRunCommandRunner::CanRunMore() const {
|
|
|
++size_t DryRunCommandRunner::CanRunMore() {
|
|
|
+ return SIZE_MAX;
|
|
|
}
|
|
|
|
|
|
+bool DryRunCommandRunner::AcquireToken() {
|
|
|
@@ -105,24 +97,25 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
if (finished_.empty())
|
|
|
return false;
|
|
|
|
|
|
-@@ -149,7 +155,7 @@ void Plan::EdgeWanted(const Edge* edge)
|
|
|
+@@ -160,7 +166,7 @@ void Plan::EdgeWanted(const Edge* edge)
|
|
|
}
|
|
|
|
|
|
Edge* Plan::FindWork() {
|
|
|
- if (ready_.empty())
|
|
|
+ if (!more_ready())
|
|
|
return NULL;
|
|
|
- EdgeSet::iterator e = ready_.begin();
|
|
|
- Edge* edge = *e;
|
|
|
-@@ -448,19 +454,39 @@ void Plan::Dump() const {
|
|
|
+
|
|
|
+ Edge* work = ready_.top();
|
|
|
+@@ -595,19 +601,39 @@ void Plan::Dump() const {
|
|
|
}
|
|
|
|
|
|
struct RealCommandRunner : public CommandRunner {
|
|
|
- explicit RealCommandRunner(const BuildConfig& config) : config_(config) {}
|
|
|
- virtual ~RealCommandRunner() {}
|
|
|
+- virtual size_t CanRunMore() const;
|
|
|
+ explicit RealCommandRunner(const BuildConfig& config);
|
|
|
+ virtual ~RealCommandRunner();
|
|
|
- virtual bool CanRunMore() const;
|
|
|
++ virtual size_t CanRunMore();
|
|
|
+ virtual bool AcquireToken();
|
|
|
virtual bool StartCommand(Edge* edge);
|
|
|
- virtual bool WaitForCommand(Result* result);
|
|
|
@@ -157,7 +150,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
vector<Edge*> RealCommandRunner::GetActiveEdges() {
|
|
|
vector<Edge*> edges;
|
|
|
for (map<const Subprocess*, Edge*>::iterator e = subproc_to_edge_.begin();
|
|
|
-@@ -471,14 +497,23 @@ vector<Edge*> RealCommandRunner::GetActi
|
|
|
+@@ -618,9 +644,11 @@ vector<Edge*> RealCommandRunner::GetActi
|
|
|
|
|
|
void RealCommandRunner::Abort() {
|
|
|
subprocs_.Clear();
|
|
|
@@ -165,28 +158,35 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+ tokens_->Clear();
|
|
|
}
|
|
|
|
|
|
- bool RealCommandRunner::CanRunMore() const {
|
|
|
-- size_t subproc_number =
|
|
|
-- subprocs_.running_.size() + subprocs_.finished_.size();
|
|
|
-- return (int)subproc_number < config_.parallelism
|
|
|
-- && ((subprocs_.running_.empty() || config_.max_load_average <= 0.0f)
|
|
|
-- || GetLoadAverage() < config_.max_load_average);
|
|
|
-+ bool parallelism_limit_not_reached =
|
|
|
-+ tokens_ || // ignore config_.parallelism
|
|
|
-+ ((int) (subprocs_.running_.size() +
|
|
|
-+ subprocs_.finished_.size()) < config_.parallelism);
|
|
|
-+ return parallelism_limit_not_reached
|
|
|
-+ && (subprocs_.running_.empty() ||
|
|
|
-+ (max_load_average_ <= 0.0f ||
|
|
|
-+ GetLoadAverage() < max_load_average_));
|
|
|
-+}
|
|
|
+-size_t RealCommandRunner::CanRunMore() const {
|
|
|
++size_t RealCommandRunner::CanRunMore() {
|
|
|
+ size_t subproc_number =
|
|
|
+ subprocs_.running_.size() + subprocs_.finished_.size();
|
|
|
+
|
|
|
+@@ -635,6 +663,13 @@ size_t RealCommandRunner::CanRunMore() c
|
|
|
+ if (capacity < 0)
|
|
|
+ capacity = 0;
|
|
|
+
|
|
|
++ if (tokens_) {
|
|
|
++ if (AcquireToken())
|
|
|
++ return SIZE_MAX;
|
|
|
++ else
|
|
|
++ capacity = 0;
|
|
|
++ }
|
|
|
+
|
|
|
-+bool RealCommandRunner::AcquireToken() {
|
|
|
-+ return (!tokens_ || tokens_->Acquire());
|
|
|
+ if (capacity == 0 && subprocs_.running_.empty())
|
|
|
+ // Ensure that we make progress.
|
|
|
+ capacity = 1;
|
|
|
+@@ -642,24 +677,42 @@ size_t RealCommandRunner::CanRunMore() c
|
|
|
+ return capacity;
|
|
|
}
|
|
|
|
|
|
++bool RealCommandRunner::AcquireToken() {
|
|
|
++ return (!tokens_ || tokens_->Acquire());
|
|
|
++}
|
|
|
++
|
|
|
bool RealCommandRunner::StartCommand(Edge* edge) {
|
|
|
-@@ -486,19 +521,33 @@ bool RealCommandRunner::StartCommand(Edg
|
|
|
+ string command = edge->EvaluateCommand();
|
|
|
Subprocess* subproc = subprocs_.Add(command, edge->use_console());
|
|
|
if (!subproc)
|
|
|
return false;
|
|
|
@@ -223,61 +223,17 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
result->status = subproc->Finish();
|
|
|
result->output = subproc->GetOutput();
|
|
|
|
|
|
-@@ -620,38 +669,43 @@ bool Builder::Build(string* err) {
|
|
|
- // command runner.
|
|
|
+@@ -790,7 +843,8 @@ bool Builder::Build(string* err) {
|
|
|
// Second, we attempt to wait for / reap the next finished command.
|
|
|
while (plan_.more_to_do()) {
|
|
|
-- // See if we can start any more commands.
|
|
|
-- if (failures_allowed && command_runner_->CanRunMore()) {
|
|
|
-- if (Edge* edge = plan_.FindWork()) {
|
|
|
-- if (edge->GetBindingBool("generator")) {
|
|
|
-+ // See if we can start any more commands...
|
|
|
-+ bool can_run_more =
|
|
|
-+ failures_allowed &&
|
|
|
-+ plan_.more_ready() &&
|
|
|
-+ command_runner_->CanRunMore();
|
|
|
-+
|
|
|
-+ // ... but we also need a token to do that.
|
|
|
-+ if (can_run_more && command_runner_->AcquireToken()) {
|
|
|
-+ Edge* edge = plan_.FindWork();
|
|
|
-+ if (edge->GetBindingBool("generator")) {
|
|
|
- scan_.build_log()->Close();
|
|
|
- }
|
|
|
-
|
|
|
-- if (!StartEdge(edge, err)) {
|
|
|
-+ if (!StartEdge(edge, err)) {
|
|
|
-+ Cleanup();
|
|
|
-+ status_->BuildFinished();
|
|
|
-+ return false;
|
|
|
-+ }
|
|
|
-+
|
|
|
-+ if (edge->is_phony()) {
|
|
|
-+ if (!plan_.EdgeFinished(edge, Plan::kEdgeSucceeded, err)) {
|
|
|
- Cleanup();
|
|
|
- status_->BuildFinished();
|
|
|
- return false;
|
|
|
- }
|
|
|
--
|
|
|
-- if (edge->is_phony()) {
|
|
|
-- if (!plan_.EdgeFinished(edge, Plan::kEdgeSucceeded, err)) {
|
|
|
-- Cleanup();
|
|
|
-- status_->BuildFinished();
|
|
|
-- return false;
|
|
|
-- }
|
|
|
-- } else {
|
|
|
-- ++pending_commands;
|
|
|
-- }
|
|
|
--
|
|
|
-- // We made some progress; go back to the main loop.
|
|
|
-- continue;
|
|
|
-+ } else {
|
|
|
-+ ++pending_commands;
|
|
|
- }
|
|
|
-+
|
|
|
-+ // We made some progress; go back to the main loop.
|
|
|
-+ continue;
|
|
|
- }
|
|
|
-
|
|
|
+ // See if we can start any more commands.
|
|
|
+- if (failures_allowed) {
|
|
|
++ bool can_run_more = failures_allowed && plan_.more_ready();
|
|
|
++ if (can_run_more) {
|
|
|
+ size_t capacity = command_runner_->CanRunMore();
|
|
|
+ while (capacity > 0) {
|
|
|
+ Edge* edge = plan_.FindWork();
|
|
|
+@@ -833,7 +887,7 @@ bool Builder::Build(string* err) {
|
|
|
// See if we can reap any finished commands.
|
|
|
if (pending_commands) {
|
|
|
CommandRunner::Result result;
|
|
|
@@ -286,7 +242,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
result.status == ExitInterrupted) {
|
|
|
Cleanup();
|
|
|
status_->BuildFinished();
|
|
|
-@@ -659,6 +713,10 @@ bool Builder::Build(string* err) {
|
|
|
+@@ -841,6 +895,10 @@ bool Builder::Build(string* err) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -299,7 +255,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
Cleanup();
|
|
|
--- a/src/build.h
|
|
|
+++ b/src/build.h
|
|
|
-@@ -52,6 +52,9 @@ struct Plan {
|
|
|
+@@ -51,6 +51,9 @@ struct Plan {
|
|
|
/// Returns true if there's more work to be done.
|
|
|
bool more_to_do() const { return wanted_edges_ > 0 && command_edges_ > 0; }
|
|
|
|
|
|
@@ -309,15 +265,17 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
/// Dumps the current state of the plan.
|
|
|
void Dump() const;
|
|
|
|
|
|
-@@ -136,6 +139,7 @@ private:
|
|
|
+@@ -145,7 +148,8 @@ private:
|
|
|
+ /// RealCommandRunner is an implementation that actually runs commands.
|
|
|
struct CommandRunner {
|
|
|
virtual ~CommandRunner() {}
|
|
|
- virtual bool CanRunMore() const = 0;
|
|
|
+- virtual size_t CanRunMore() const = 0;
|
|
|
++ virtual size_t CanRunMore() = 0;
|
|
|
+ virtual bool AcquireToken() = 0;
|
|
|
virtual bool StartCommand(Edge* edge) = 0;
|
|
|
|
|
|
/// The result of waiting for a command.
|
|
|
-@@ -147,7 +151,9 @@ struct CommandRunner {
|
|
|
+@@ -157,7 +161,9 @@ struct CommandRunner {
|
|
|
bool success() const { return status == ExitSuccess; }
|
|
|
};
|
|
|
/// Wait for a command to complete, or return false if interrupted.
|
|
|
@@ -328,7 +286,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
|
|
|
virtual std::vector<Edge*> GetActiveEdges() { return std::vector<Edge*>(); }
|
|
|
virtual void Abort() {}
|
|
|
-@@ -155,7 +161,8 @@ struct CommandRunner {
|
|
|
+@@ -165,7 +171,8 @@ struct CommandRunner {
|
|
|
|
|
|
/// Options (e.g. verbosity, parallelism) passed to a build.
|
|
|
struct BuildConfig {
|
|
|
@@ -338,7 +296,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
failures_allowed(1), max_load_average(-0.0f) {}
|
|
|
|
|
|
enum Verbosity {
|
|
|
-@@ -167,6 +174,7 @@ struct BuildConfig {
|
|
|
+@@ -177,6 +184,7 @@ struct BuildConfig {
|
|
|
Verbosity verbosity;
|
|
|
bool dry_run;
|
|
|
int parallelism;
|
|
|
@@ -509,13 +467,15 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
|
|
|
#include <assert.h>
|
|
|
+#include <stdarg.h>
|
|
|
+ #include <climits>
|
|
|
+ #include <stdint.h>
|
|
|
|
|
|
- #include "build_log.h"
|
|
|
- #include "deps_log.h"
|
|
|
-@@ -474,8 +475,9 @@ struct FakeCommandRunner : public Comman
|
|
|
+@@ -521,9 +522,10 @@ struct FakeCommandRunner : public Comman
|
|
|
+ max_active_edges_(1), fs_(fs) {}
|
|
|
|
|
|
// CommandRunner impl
|
|
|
- virtual bool CanRunMore() const;
|
|
|
+- virtual size_t CanRunMore() const;
|
|
|
++ virtual size_t CanRunMore();
|
|
|
+ virtual bool AcquireToken();
|
|
|
virtual bool StartCommand(Edge* edge);
|
|
|
- virtual bool WaitForCommand(Result* result);
|
|
|
@@ -523,8 +483,16 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
virtual vector<Edge*> GetActiveEdges();
|
|
|
virtual void Abort();
|
|
|
|
|
|
-@@ -578,6 +580,10 @@ bool FakeCommandRunner::CanRunMore() con
|
|
|
- return active_edges_.size() < max_active_edges_;
|
|
|
+@@ -622,13 +624,17 @@ void BuildTest::RebuildTarget(const stri
|
|
|
+ builder.command_runner_.release();
|
|
|
+ }
|
|
|
+
|
|
|
+-size_t FakeCommandRunner::CanRunMore() const {
|
|
|
++size_t FakeCommandRunner::CanRunMore() {
|
|
|
+ if (active_edges_.size() < max_active_edges_)
|
|
|
+ return SIZE_MAX;
|
|
|
+
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
+bool FakeCommandRunner::AcquireToken() {
|
|
|
@@ -534,7 +502,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
bool FakeCommandRunner::StartCommand(Edge* edge) {
|
|
|
assert(active_edges_.size() < max_active_edges_);
|
|
|
assert(find(active_edges_.begin(), active_edges_.end(), edge)
|
|
|
-@@ -649,7 +655,7 @@ bool FakeCommandRunner::StartCommand(Edg
|
|
|
+@@ -720,7 +726,7 @@ bool FakeCommandRunner::StartCommand(Edg
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -543,7 +511,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
if (active_edges_.empty())
|
|
|
return false;
|
|
|
|
|
|
-@@ -3985,3 +3991,356 @@ TEST_F(BuildTest, ValidationWithCircular
|
|
|
+@@ -4380,3 +4386,355 @@ TEST_F(BuildTest, ValidationWithCircular
|
|
|
EXPECT_FALSE(builder_.AddTarget("out", &err));
|
|
|
EXPECT_EQ("dependency cycle: validate -> validate_in -> validate", err);
|
|
|
}
|
|
|
@@ -557,7 +525,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+ explicit FakeTokenCommandRunner() {}
|
|
|
+
|
|
|
+ // CommandRunner impl
|
|
|
-+ virtual bool CanRunMore() const;
|
|
|
++ virtual bool CanRunMore();
|
|
|
+ virtual bool AcquireToken();
|
|
|
+ virtual bool StartCommand(Edge* edge);
|
|
|
+ virtual bool WaitForCommand(Result* result, bool more_ready);
|
|
|
@@ -572,7 +540,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+ vector<bool> wait_for_command_;
|
|
|
+};
|
|
|
+
|
|
|
-+bool FakeTokenCommandRunner::CanRunMore() const {
|
|
|
++bool FakeTokenCommandRunner::CanRunMore() {
|
|
|
+ if (can_run_more_.size() == 0) {
|
|
|
+ EXPECT_FALSE("unexpected call to CommandRunner::CanRunMore()");
|
|
|
+ return false;
|
|
|
@@ -580,9 +548,8 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+
|
|
|
+ bool result = can_run_more_[0];
|
|
|
+
|
|
|
-+ // Unfortunately CanRunMore() isn't "const" for tests
|
|
|
-+ const_cast<FakeTokenCommandRunner*>(this)->can_run_more_.erase(
|
|
|
-+ const_cast<FakeTokenCommandRunner*>(this)->can_run_more_.begin()
|
|
|
++ can_run_more_.erase(
|
|
|
++ can_run_more_.begin()
|
|
|
+ );
|
|
|
+
|
|
|
+ return result;
|
|
|
@@ -1345,7 +1312,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+}
|
|
|
--- a/src/ninja.cc
|
|
|
+++ b/src/ninja.cc
|
|
|
-@@ -1447,6 +1447,7 @@ int ReadFlags(int* argc, char*** argv,
|
|
|
+@@ -1466,6 +1466,7 @@ int ReadFlags(int* argc, char*** argv,
|
|
|
// We want to run N jobs in parallel. For N = 0, INT_MAX
|
|
|
// is close enough to infinite for most sane builds.
|
|
|
config->parallelism = value > 0 ? value : INT_MAX;
|
|
|
@@ -2139,7 +2106,7 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
+};
|
|
|
--- a/CMakeLists.txt
|
|
|
+++ b/CMakeLists.txt
|
|
|
-@@ -112,6 +112,7 @@ add_library(libninja OBJECT
|
|
|
+@@ -142,6 +142,7 @@ add_library(libninja OBJECT
|
|
|
src/state.cc
|
|
|
src/status.cc
|
|
|
src/string_piece_util.cc
|
|
|
@@ -2147,22 +2114,26 @@ Fixes https://github.com/ninja-build/ninja/issues/1139
|
|
|
src/util.cc
|
|
|
src/version.cc
|
|
|
)
|
|
|
-@@ -123,9 +124,14 @@ if(WIN32)
|
|
|
+@@ -153,13 +154,17 @@ if(WIN32)
|
|
|
src/msvc_helper_main-win32.cc
|
|
|
src/getopt.c
|
|
|
src/minidump-win32.cc
|
|
|
+ src/tokenpool-gnu-make-win32.cc
|
|
|
)
|
|
|
+ # Build getopt.c, which can be compiled as either C or C++, as C++
|
|
|
+ # so that build environments which lack a C compiler, but have a C++
|
|
|
+ # compiler may build ninja.
|
|
|
+ set_source_files_properties(src/getopt.c PROPERTIES LANGUAGE CXX)
|
|
|
else()
|
|
|
- target_sources(libninja PRIVATE src/subprocess-posix.cc)
|
|
|
+- target_sources(libninja PRIVATE src/subprocess-posix.cc)
|
|
|
+ target_sources(libninja PRIVATE
|
|
|
+ src/subprocess-posix.cc
|
|
|
+ src/tokenpool-gnu-make-posix.cc
|
|
|
+ )
|
|
|
if(CMAKE_SYSTEM_NAME STREQUAL "OS400" OR CMAKE_SYSTEM_NAME STREQUAL "AIX")
|
|
|
target_sources(libninja PRIVATE src/getopt.c)
|
|
|
- endif()
|
|
|
-@@ -204,6 +210,7 @@ if(BUILD_TESTING)
|
|
|
+ # Build getopt.c, which can be compiled as either C or C++, as C++
|
|
|
+@@ -286,6 +291,7 @@ if(BUILD_TESTING)
|
|
|
src/string_piece_util_test.cc
|
|
|
src/subprocess_test.cc
|
|
|
src/test.cc
|