]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Report SMP store queues state (mgr:store_queues) (#690)
authorEduard Bagdasaryan <eduard.bagdasaryan@measurement-factory.com>
Fri, 10 Jul 2020 16:45:50 +0000 (16:45 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Tue, 14 Jul 2020 05:30:42 +0000 (05:30 +0000)
The state of two Store queues is reported: Transients notification queue
(a.k.a. Collapsed Forwarding queue) and SMP I/O request queue (used for
communication with rock diskers). Each worker and disker kid reports its
view of that kid's incoming and outgoing queues state, including a small
sample (up to 7 items) of queued messages. These kid-specific reports
are YAML-compliant.

With the exception of a field labeled "other", each queue report is
self-consistent despite accessing data shared among kids -- the reported
combination of values did exist at the snapshot collection time.

The special "other" field represents a message counter maintained by the
other side of the queue. In most cases, that field will be close to its
correct value, but, due to modifications by the other process of a
non-atomic value, it may be virtually anything. We think it is better to
report (without officially naming) this field because it may be useful
in triage regardless of these caveats. Making the counter atomic just
for these occasional reports is not worth the performance overheads.

Also fixed testStore linking problems (on some platforms) that were
caused by the wrong order of libdiskio and libipc dependencies.

16 files changed:
src/CollapsedForwarding.cc
src/CollapsedForwarding.h
src/DiskIO/IpcIo/IpcIoFile.cc
src/DiskIO/IpcIo/IpcIoFile.h
src/Makefile.am
src/MemObject.cc
src/SquidTime.h
src/icmp/Icmp.cc
src/ipc/Queue.cc
src/ipc/Queue.h
src/log/access_log.cc
src/store.cc
src/tests/stub_CollapsedForwarding.cc
src/tests/stub_IpcIoFile.cc [new file with mode: 0644]
src/tests/stub_time.cc
src/time.cc

index 9fc2c090470174d070a1e41b692c4208ee49ebcd..1169aa7582d2c054159ddb777c95a4491727ba6d 100644 (file)
@@ -35,6 +35,9 @@ class CollapsedForwardingMsg
 public:
     CollapsedForwardingMsg(): sender(-1), xitIndex(-1) {}
 
+    /// prints message parameters; suitable for cache manager reports
+    void stat(std::ostream &);
+
 public:
     int sender; ///< kid ID of sending process
 
@@ -42,6 +45,12 @@ public:
     sfileno xitIndex;
 };
 
+void
+CollapsedForwardingMsg::stat(std::ostream &os)
+{
+    os << "sender: " << sender << ", xitIndex: " << xitIndex;
+}
+
 // CollapsedForwarding
 
 void
@@ -137,6 +146,15 @@ CollapsedForwarding::HandleNotification(const Ipc::TypedMsgHdr &msg)
     HandleNewData("after notification");
 }
 
+void
+CollapsedForwarding::StatQueue(std::ostream &os)
+{
+    if (queue.get()) {
+        os << "Transients queues:\n";
+        queue->stat<CollapsedForwardingMsg>(os);
+    }
+}
+
 /// initializes shared queue used by CollapsedForwarding
 class CollapsedForwardingRr: public Ipc::Mem::RegisteredRunner
 {
index 4c2e1af3e8c141a550183fd638e6c03a4a6b2443..e7b226fffd7b24e9e180c4f6482054d098b8b302 100644 (file)
@@ -42,6 +42,9 @@ public:
     /// handle queue push notifications from worker or disker
     static void HandleNotification(const Ipc::TypedMsgHdr &msg);
 
+    /// prints IPC message queue state; suitable for cache manager reports
+    static void StatQueue(std::ostream &);
+
 private:
     typedef Ipc::MultiQueue Queue;
     static std::unique_ptr<Queue> queue; ///< IPC queue
index 00aec4d8d48c090a48df6d9ac41f987285ee5448..e97487de59583f485de7e262a5a95c232f62f2eb 100644 (file)
@@ -67,9 +67,30 @@ std::ostream &
 operator <<(std::ostream &os, const SipcIo &sio)
 {
     return os << "ipcIo" << sio.worker << '.' << sio.msg.requestId <<
-           (sio.msg.command == IpcIo::cmdRead ? 'r' : 'w') << sio.disker;
+           sio.msg.command << sio.disker;
 }
 
+/* IpcIo::Command */
+
+std::ostream &
+operator <<(std::ostream &os, const IpcIo::Command command)
+{
+    switch (command) {
+    case IpcIo::cmdNone:
+        return os << '-';
+    case IpcIo::cmdOpen:
+        return os << 'o';
+    case IpcIo::cmdRead:
+        return os << 'r';
+    case IpcIo::cmdWrite:
+        return os << 'w';
+    }
+    // unreachable code
+    return os << static_cast<int>(command);
+}
+
+/* IpcIoFile */
+
 IpcIoFile::IpcIoFile(char const *aDb):
     dbName(aDb), diskId(-1), error_(false), lastRequestId(0),
     olderRequests(&requestMap1), newerRequests(&requestMap2),
@@ -501,6 +522,15 @@ IpcIoFile::HandleNotification(const Ipc::TypedMsgHdr &msg)
         HandleResponses("after notification");
 }
 
+void
+IpcIoFile::StatQueue(std::ostream &os)
+{
+    if (queue.get()) {
+        os << "SMP disk I/O queues:\n";
+        queue->stat<IpcIoMsg>(os);
+    }
+}
+
 /// handles open request timeout
 void
 IpcIoFile::OpenTimeout(void *const param)
@@ -632,6 +662,21 @@ IpcIoMsg::IpcIoMsg():
     start.tv_usec = 0;
 }
 
+void
+IpcIoMsg::stat(std::ostream &os)
+{
+    timeval elapsedTime;
+    tvSub(elapsedTime, start, current_time);
+    os << "id: " << requestId <<
+        ", offset: " << offset <<
+        ", size: " << len <<
+        ", page: " << page <<
+        ", command: " << command <<
+        ", start: " << start <<
+        ", elapsed: " << elapsedTime <<
+        ", errno: " << xerrno;
+}
+
 /* IpcIoPendingRequest */
 
 IpcIoPendingRequest::IpcIoPendingRequest(const IpcIoFile::Pointer &aFile):
index 2589f292632f1b83c4d96c45107263d76eb26886..1c5e5c4ae869159d487f2aa9960c666b7b5297c0 100644 (file)
@@ -34,12 +34,17 @@ typedef enum { cmdNone, cmdOpen, cmdRead, cmdWrite } Command;
 
 } // namespace IpcIo
 
+std::ostream &operator <<(std::ostream &, IpcIo::Command);
+
 /// converts DiskIO requests to IPC queue messages
 class IpcIoMsg
 {
 public:
     IpcIoMsg();
 
+    /// prints message parameters; suitable for cache manager reports
+    void stat(std::ostream &);
+
 public:
     unsigned int requestId; ///< unique for requestor; matches request w/ response
 
@@ -86,6 +91,9 @@ public:
     /// handle queue push notifications from worker or disker
     static void HandleNotification(const Ipc::TypedMsgHdr &msg);
 
+    /// prints IPC message queue state; suitable for cache manager reports
+    static void StatQueue(std::ostream &);
+
     DiskFile::Config config; ///< supported configuration options
 
 protected:
index 6452db0abd384a73c20b48c0c12894b5cb76522f..8b2278def8a35eab44f3e5c2f7c8d4232394755d 100644 (file)
@@ -1595,12 +1595,12 @@ tests_testStore_LDADD= \
        ip/libip.la \
        fs/libfs.la \
        mgr/libmgr.la \
-       ipc/libipc.la \
        anyp/libanyp.la \
        mem/libmem.la \
        store/libstore.la \
        sbuf/libsbuf.la \
        DiskIO/libdiskio.la \
+       ipc/libipc.la \
        $(top_builddir)/lib/libmisccontainers.la \
        $(top_builddir)/lib/libmiscencoding.la \
        $(top_builddir)/lib/libmiscutil.la \
@@ -1951,6 +1951,7 @@ tests_test_http_range_SOURCES = \
        event.cc \
        tests/stub_external_acl.cc \
        tests/stub_fatal.cc \
+       tests/stub_IpcIoFile.cc \
        fatal.h \
        fd.cc \
        fd.h \
@@ -2368,6 +2369,7 @@ tests_testHttpRequest_SOURCES = \
        tests/stub_libsecurity.cc \
        tests/stub_libstore.cc \
        tests/stub_main_cc.cc \
+       tests/stub_IpcIoFile.cc \
        mem_node.cc \
        mime.cc \
        mime.h \
@@ -2670,6 +2672,7 @@ tests_testCacheManager_SOURCES = \
        tests/stub_libsecurity.cc \
        tests/stub_libstore.cc \
        tests/stub_main_cc.cc \
+       tests/stub_IpcIoFile.cc \
        mem_node.cc \
        mime.cc \
        mime.h \
@@ -2992,6 +2995,7 @@ tests_testEvent_SOURCES = \
        tests/stub_libsecurity.cc \
        tests/stub_libstore.cc \
        tests/stub_main_cc.cc \
+       tests/stub_IpcIoFile.cc \
        mem_node.cc \
        mime.cc \
        mime.h \
index 00fc25589bceffcd8a17bceea98669de59e51d39..e54aa04372b85cd2e8be4bfef6962d5b1ab0d984 100644 (file)
@@ -171,7 +171,7 @@ MemObject::dump() const
     debugs(20, DBG_IMPORTANT, "MemObject->data.origin_offset: " << (data_hdr.head ? data_hdr.head->nodeBuffer.offset : 0));
 #endif
 
-    debugs(20, DBG_IMPORTANT, "MemObject->start_ping: " << start_ping.tv_sec  << "."<< std::setfill('0') << std::setw(6) << start_ping.tv_usec);
+    debugs(20, DBG_IMPORTANT, "MemObject->start_ping: " << start_ping);
     debugs(20, DBG_IMPORTANT, "MemObject->inmem_hi: " << data_hdr.endOffset());
     debugs(20, DBG_IMPORTANT, "MemObject->inmem_lo: " << inmem_lo);
     debugs(20, DBG_IMPORTANT, "MemObject->nclients: " << nclients);
index eda43890b8b4c9cbdd21220e83be6ce49447e5a9..111e9fd20dada39fa185942d18a4312f64fa8c92 100644 (file)
@@ -14,6 +14,7 @@
 #include "rfc1123.h"
 
 #include <ctime>
+#include <iosfwd>
 /* NP: sys/time.h is provided by libcompat */
 
 /* Use uint64_t to store milliseconds */
@@ -97,6 +98,9 @@ operator ==(const timeval &a, const timeval &b)
     return !(a != b);
 }
 
+/// prints <seconds>.<microseconds>
+std::ostream &operator <<(std::ostream &, const timeval &);
+
 namespace Time
 {
 
index 39a4408a7eb38193ce03d505cf138e7c897ea512..c695b029cae9fa95b9cd1474aaedd5957e1937d8 100644 (file)
@@ -87,9 +87,7 @@ Icmp::ipHops(int ttl)
 void
 Icmp::Log(const Ip::Address &addr, const uint8_t type, const char* pkt_str, const int rtt, const int hops)
 {
-    debugs(42, 2, "pingerLog: " << std::setw(9) << current_time.tv_sec  <<
-           "." << std::setfill('0') << std::setw(6) <<
-           current_time.tv_usec  << " " << std::left << std::setfill(' ') <<
+    debugs(42, 2, "pingerLog: " << current_time << " " << std::left <<
            std::setw(45) << addr  << " " << type  <<
            " " << std::setw(15) << pkt_str << " " << rtt  <<
            "ms " << hops  << " hops");
index 20e8586df2422906a65042c963a77852393fa7d0..6184f85de981c052492aeda108e3ac5fa281bbf2 100644 (file)
@@ -95,6 +95,25 @@ Ipc::OneToOneUniQueue::Items2Bytes(const unsigned int maxItemSize, const int siz
     return sizeof(OneToOneUniQueue) + maxItemSize * size;
 }
 
+/// start state reporting (by reporting queue parameters)
+/// The labels reflect whether the caller owns theIn or theOut data member and,
+/// hence, cannot report the other value reliably.
+void
+Ipc::OneToOneUniQueue::statOpen(std::ostream &os, const char *inLabel, const char *outLabel, const uint32_t count) const
+{
+    os << "{ size: " << count <<
+        ", capacity: " << theCapacity <<
+        ", " << inLabel << ": " << theIn <<
+        ", " << outLabel << ": " << theOut;
+}
+
+/// end state reporting started by statOpen()
+void
+Ipc::OneToOneUniQueue::statClose(std::ostream &os) const
+{
+    os << "}\n";
+}
+
 /* OneToOneUniQueues */
 
 Ipc::OneToOneUniQueues::OneToOneUniQueues(const int aCapacity, const unsigned int maxItemSize, const int queueCapacity): theCapacity(aCapacity)
index 4bd87bf3f638a8ca1d9ddb0410afb05682385e02..5ada9fdb338ce0cf696109256e3cd796f230f293 100644 (file)
@@ -15,6 +15,7 @@
 #include "ipc/mem/Pointer.h"
 #include "util.h"
 
+#include <algorithm>
 #include <atomic>
 
 class String;
@@ -114,10 +115,21 @@ public:
     /// returns true iff the value was set; the value may be stale!
     template<class Value> bool peek(Value &value) const;
 
+    /// prints incoming queue state; suitable for cache manager reports
+    template<class Value> void statIn(std::ostream &, int localProcessId, int remoteProcessId) const;
+    /// prints outgoing queue state; suitable for cache manager reports
+    template<class Value> void statOut(std::ostream &, int localProcessId, int remoteProcessId) const;
+
 private:
+    void statOpen(std::ostream &, const char *inLabel, const char *outLabel, uint32_t count) const;
+    void statClose(std::ostream &) const;
+    template<class Value> void statSamples(std::ostream &, unsigned int start, uint32_t size) const;
+    template<class Value> void statRange(std::ostream &, unsigned int start, uint32_t n) const;
 
-    unsigned int theIn; ///< input index, used only in push()
-    unsigned int theOut; ///< output index, used only in pop()
+    // optimization: these non-std::atomic data members are in shared memory,
+    // but each is used only by one process (aside from obscured reporting)
+    unsigned int theIn; ///< current push() position; reporting aside, used only in push()
+    unsigned int theOut; ///< current pop() position; reporting aside, used only in pop()/peek()
 
     std::atomic<uint32_t> theSize; ///< number of items in the queue
     const unsigned int theMaxItemSize; ///< maximum item size
@@ -167,6 +179,9 @@ public:
     /// peeks at the item likely to be pop()ed next
     template<class Value> bool peek(int &remoteProcessId, Value &value) const;
 
+    /// prints current state; suitable for cache manager reports
+    template<class Value> void stat(std::ostream &) const;
+
     /// returns local reader's balance
     QueueReader::Balance &localBalance() { return localReader().balance; }
 
@@ -410,6 +425,92 @@ OneToOneUniQueue::push(const Value &value, QueueReader *const reader)
     return wasEmpty && (!reader || reader->raiseSignal());
 }
 
+template <class Value>
+void
+OneToOneUniQueue::statIn(std::ostream &os, const int localProcessId, const int remoteProcessId) const
+{
+    os << "  kid" << localProcessId << " receiving from kid" << remoteProcessId << ": ";
+    // Nobody can modify our theOut so, after capturing some valid theSize value
+    // in count, we can reliably report all [theOut, theOut+count) items that
+    // were queued at theSize capturing time. We will miss new items push()ed by
+    // the other side, but it is OK -- we report state at the capturing time.
+    const auto count = theSize.load();
+    statOpen(os, "other", "popIndex", count);
+    statSamples<Value>(os, theOut, count);
+    statClose(os);
+}
+
+template <class Value>
+void
+OneToOneUniQueue::statOut(std::ostream &os, const int localProcessId, const int remoteProcessId) const
+{
+    os << "  kid" << localProcessId << " sending to kid" << remoteProcessId << ": ";
+    // Nobody can modify our theIn so, after capturing some valid theSize value
+    // in count, we can reliably report all [theIn-count, theIn) items that were
+    // queued at theSize capturing time. We may report items already pop()ed by
+    // the other side, but that is OK because pop() does not modify items -- it
+    // only increments theOut.
+    const auto count = theSize.load();
+    statOpen(os, "pushIndex", "other", count);
+    statSamples<Value>(os, theIn - count, count); // unsigned offset underflow OK
+    statClose(os);
+}
+
+/// report a sample of [start, start + size) items
+template <class Value>
+void
+OneToOneUniQueue::statSamples(std::ostream &os, const unsigned int start, const uint32_t count) const
+{
+    if (!count) {
+        os << " ";
+        return;
+    }
+
+    os << ", items: [\n";
+    // report a few leading and trailing items, without repetitions
+    const auto sampleSize = std::min(3U, count); // leading (and max) sample
+    statRange<Value>(os, start, sampleSize);
+    if (sampleSize < count) { // the first sample did not show some items
+        // The `start` offset aside, the first sample reported all items
+        // below the sampleSize offset. The second sample needs to report
+        // the last sampleSize items (i.e. starting at count-sampleSize
+        // offset) except those already reported by the first sample.
+        const auto secondSampleOffset = std::max(sampleSize, count - sampleSize);
+        const auto secondSampleSize = std::min(sampleSize, count - sampleSize);
+
+        // but first we print a sample separator, unless there are no items
+        // between the samples or the separator hides the only unsampled item
+        const auto bothSamples = sampleSize + secondSampleSize;
+        if (bothSamples + 1U == count)
+            statRange<Value>(os, start + sampleSize, 1);
+        else if (count > bothSamples)
+            os << "    # ... " << (count - bothSamples) << " items not shown ...\n";
+
+        statRange<Value>(os, start + secondSampleOffset, secondSampleSize);
+    }
+    os << "  ]";
+}
+
+/// statSamples() helper that reports n items from start
+template <class Value>
+void
+OneToOneUniQueue::statRange(std::ostream &os, const unsigned int start, const uint32_t n) const
+{
+    assert(sizeof(Value) <= theMaxItemSize);
+    auto offset = start;
+    for (uint32_t i = 0; i < n; ++i) {
+        // XXX: Throughout this C++ header, these overflow wrapping tricks work
+        // only because theCapacity currently happens to be a power of 2 (e.g.,
+        // the highest offset (0xF...FFF) % 3 is 0 and so is the next offset).
+        const auto pos = (offset++ % theCapacity) * theMaxItemSize;
+        Value value;
+        memcpy(&value, theBuffer + pos, sizeof(value));
+        os << "    { ";
+        value.stat(os);
+        os << " },\n";
+    }
+}
+
 // OneToOneUniQueues
 
 inline OneToOneUniQueue &
@@ -474,6 +575,23 @@ BaseMultiQueue::peek(int &remoteProcessId, Value &value) const
     return false; // most likely, no process had anything to pop
 }
 
+template <class Value>
+void
+BaseMultiQueue::stat(std::ostream &os) const
+{
+    for (int processId = remotesIdOffset(); processId < remotesIdOffset() + remotesCount(); ++processId) {
+        const auto &queue = inQueue(processId);
+        queue.statIn<Value>(os, theLocalProcessId, processId);
+    }
+
+    os << "\n";
+
+    for (int processId = remotesIdOffset(); processId < remotesIdOffset() + remotesCount(); ++processId) {
+        const auto &queue = outQueue(processId);
+        queue.statOut<Value>(os, theLocalProcessId, processId);
+    }
+}
+
 // FewToFewBiQueue
 
 template <class Value>
index e64dd7641444e3f774220ce20a8385b8157246ff..38887dd10eb7cd67e7e2e999152f85bbe4085058 100644 (file)
@@ -302,8 +302,7 @@ HierarchyLogEntry::startPeerClock()
 void
 HierarchyLogEntry::stopPeerClock(const bool force)
 {
-    debugs(46, 5, "First connection started: " << firstConnStart_.tv_sec << "." <<
-           std::setfill('0') << std::setw(6) << firstConnStart_.tv_usec <<
+    debugs(46, 5, "First connection started: " << firstConnStart_ <<
            ", current total response time value: " << (totalResponseTime_.tv_sec * 1000 +  totalResponseTime_.tv_usec/1000) <<
            (force ? ", force fixing" : ""));
     if (!force && totalResponseTime_.tv_sec != -1)
index c69468f822208a93149231b3c62c00dc2569b7e5..094c00c1a265825bbad4d811b5f29ec81706d2a7 100644 (file)
 
 #include "squid.h"
 #include "base/AsyncCbdataCalls.h"
+#include "base/PackableStream.h"
 #include "base/TextException.h"
 #include "CacheDigest.h"
 #include "CacheManager.h"
+#include "CollapsedForwarding.h"
 #include "comm/Connection.h"
 #include "comm/Read.h"
+#if HAVE_DISKIO_MODULE_IPCIO
+#include "DiskIO/IpcIo/IpcIoFile.h"
+#endif
 #include "ETag.h"
 #include "event.h"
 #include "fde.h"
@@ -122,6 +127,20 @@ Store::Stats(StoreEntry * output)
     Root().stat(*output);
 }
 
+/// reports the current state of Store-related queues
+static void
+StatQueues(StoreEntry *e)
+{
+    assert(e);
+    PackableStream stream(*e);
+    CollapsedForwarding::StatQueue(stream);
+ #if HAVE_DISKIO_MODULE_IPCIO
+    stream << "\n";
+    IpcIoFile::StatQueue(stream);
+ #endif
+    stream.flush();
+}
+
 // XXX: new/delete operators need to be replaced with MEMPROXY_CLASS
 // definitions but doing so exposes bug 4370, and maybe 4354 and 4355
 void *
@@ -1285,6 +1304,7 @@ storeRegisterWithCacheManager(void)
     Mgr::RegisterAction("store_io", "Store IO Interface Stats", &Mgr::StoreIoAction::Create, 0, 1);
     Mgr::RegisterAction("store_check_cachable_stats", "storeCheckCachable() Stats",
                         storeCheckCachableStats, 0, 1);
+    Mgr::RegisterAction("store_queues", "SMP Transients and Caching Queues", StatQueues, 0, 1);
 }
 
 void
index ff5453f87679b658dc9c13e7cf42c7b30a13cb50..b5a1dd1f94a0980fe65807dd86ecb14e340a6e0b 100644 (file)
@@ -14,4 +14,5 @@
 
 void CollapsedForwarding::Broadcast(StoreEntry const&, const bool) STUB
 void CollapsedForwarding::Broadcast(const sfileno, const bool) STUB
+void CollapsedForwarding::StatQueue(std::ostream &) STUB
 
diff --git a/src/tests/stub_IpcIoFile.cc b/src/tests/stub_IpcIoFile.cc
new file mode 100644 (file)
index 0000000..b8554e1
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+
+#if HAVE_DISKIO_MODULE_IPCIO
+#include "DiskIO/IpcIo/IpcIoFile.h"
+
+#define STUB_API "DiskIO/IocIo/IpcIoFile.cc"
+
+#include "tests/STUB.h"
+
+void IpcIoFile::StatQueue(std::ostream &) STUB
+
+#endif /* HAVE_DISKIO_MODULE_IPCIO */
+
index a5a8e6b466abe45f0668420c149f32ef7634e381..35e1bdf24f70849789bed6fc7d4abb954191f66b 100644 (file)
@@ -27,3 +27,5 @@ const char * Time::FormatHttpd(time_t ) STUB_RETVAL("")
 void TimeEngine::tick() STUB
 TimeEngine::~TimeEngine() {STUB_NOP}
 
+std::ostream &operator <<(std::ostream &os, const timeval &) STUB_RETVAL(os)
+
index 815b1c75e046b048ea98c11d3764c0817302b188..51ae3db5c070093b439e0106bab75b5454581715 100644 (file)
@@ -11,6 +11,9 @@
 #include "squid.h"
 #include "SquidTime.h"
 
+#include <iomanip>
+#include <ostream>
+
 struct timeval current_time;
 double current_dtime;
 time_t squid_curtime = 0;
@@ -78,6 +81,16 @@ TimeEngine::tick()
     getCurrentTime();
 }
 
+std::ostream &
+operator <<(std::ostream &os, const timeval &t)
+{
+    os << t.tv_sec << ".";
+    const auto savedFill = os.fill('0');
+    os << std::setw(6) << t.tv_usec;
+    os.fill(savedFill);
+    return os;
+}
+
 const char *
 Time::FormatStrf(time_t t)
 {