mirror of
https://github.com/MariaDB/server.git
synced 2025-08-07 00:04:31 +03:00
NDB tux optim 15 - fix wasted space in index node entries
ndb/src/kernel/blocks/dbtux/Dbtux.hpp: tux optim 15 - fix wasted space in index node entries ndb/src/kernel/blocks/dbtux/DbtuxDebug.cpp: tux optim 15 - fix wasted space in index node entries ndb/src/kernel/blocks/dbtux/DbtuxGen.cpp: tux optim 15 - fix wasted space in index node entries ndb/src/kernel/blocks/dbtux/DbtuxMeta.cpp: tux optim 15 - fix wasted space in index node entries ndb/src/kernel/blocks/dbtux/DbtuxNode.cpp: tux optim 15 - fix wasted space in index node entries ndb/src/kernel/blocks/dbtux/Times.txt: tux optim 15 - fix wasted space in index node entries ndb/test/ndbapi/testOIBasic.cpp: tux optim 15 - fix wasted space in index node entries
This commit is contained in:
@@ -172,12 +172,21 @@ private:
|
|||||||
* Physical tuple address in TUP. Provides fast access to table tuple
|
* Physical tuple address in TUP. Provides fast access to table tuple
|
||||||
* or index node. Valid within the db node and across timeslices.
|
* or index node. Valid within the db node and across timeslices.
|
||||||
* Not valid between db nodes or across restarts.
|
* Not valid between db nodes or across restarts.
|
||||||
|
*
|
||||||
|
* To avoid wasting an Uint16 the pageid is split in two.
|
||||||
*/
|
*/
|
||||||
struct TupLoc {
|
struct TupLoc {
|
||||||
Uint32 m_pageId; // page i-value
|
private:
|
||||||
|
Uint16 m_pageId1; // page i-value (big-endian)
|
||||||
|
Uint16 m_pageId2;
|
||||||
Uint16 m_pageOffset; // page offset in words
|
Uint16 m_pageOffset; // page offset in words
|
||||||
|
public:
|
||||||
TupLoc();
|
TupLoc();
|
||||||
TupLoc(Uint32 pageId, Uint16 pageOffset);
|
TupLoc(Uint32 pageId, Uint16 pageOffset);
|
||||||
|
Uint32 getPageId() const;
|
||||||
|
void setPageId(Uint32 pageId);
|
||||||
|
Uint32 getPageOffset() const;
|
||||||
|
void setPageOffset(Uint32 pageOffset);
|
||||||
bool operator==(const TupLoc& loc) const;
|
bool operator==(const TupLoc& loc) const;
|
||||||
bool operator!=(const TupLoc& loc) const;
|
bool operator!=(const TupLoc& loc) const;
|
||||||
};
|
};
|
||||||
@@ -224,18 +233,13 @@ private:
|
|||||||
* work entry part 5
|
* work entry part 5
|
||||||
*
|
*
|
||||||
* There are 3 links to other nodes: left child, right child, parent.
|
* There are 3 links to other nodes: left child, right child, parent.
|
||||||
* These are in TupLoc format but the pageIds and pageOffsets are
|
|
||||||
* stored in separate arrays (saves 1 word).
|
|
||||||
*
|
|
||||||
* Occupancy (number of entries) is at least 1 except temporarily when
|
* Occupancy (number of entries) is at least 1 except temporarily when
|
||||||
* a node is about to be removed. If occupancy is 1, only max entry
|
* a node is about to be removed.
|
||||||
* is present but both min and max prefixes are set.
|
|
||||||
*/
|
*/
|
||||||
struct TreeNode;
|
struct TreeNode;
|
||||||
friend struct TreeNode;
|
friend struct TreeNode;
|
||||||
struct TreeNode {
|
struct TreeNode {
|
||||||
Uint32 m_linkPI[3]; // link to 0-left child 1-right child 2-parent
|
TupLoc m_link[3]; // link to 0-left child 1-right child 2-parent
|
||||||
Uint16 m_linkPO[3]; // page offsets for above real page ids
|
|
||||||
unsigned m_side : 2; // we are 0-left child 1-right child 2-root
|
unsigned m_side : 2; // we are 0-left child 1-right child 2-root
|
||||||
int m_balance : 2; // balance -1, 0, +1
|
int m_balance : 2; // balance -1, 0, +1
|
||||||
unsigned pad1 : 4;
|
unsigned pad1 : 4;
|
||||||
@@ -805,22 +809,52 @@ Dbtux::ConstData::operator=(Data data)
|
|||||||
|
|
||||||
inline
|
inline
|
||||||
Dbtux::TupLoc::TupLoc() :
|
Dbtux::TupLoc::TupLoc() :
|
||||||
m_pageId(RNIL),
|
m_pageId1(RNIL >> 16),
|
||||||
|
m_pageId2(RNIL & 0xFFFF),
|
||||||
m_pageOffset(0)
|
m_pageOffset(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
inline
|
inline
|
||||||
Dbtux::TupLoc::TupLoc(Uint32 pageId, Uint16 pageOffset) :
|
Dbtux::TupLoc::TupLoc(Uint32 pageId, Uint16 pageOffset) :
|
||||||
m_pageId(pageId),
|
m_pageId1(pageId >> 16),
|
||||||
|
m_pageId2(pageId & 0xFFFF),
|
||||||
m_pageOffset(pageOffset)
|
m_pageOffset(pageOffset)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Uint32
|
||||||
|
Dbtux::TupLoc::getPageId() const
|
||||||
|
{
|
||||||
|
return (m_pageId1 << 16) | m_pageId2;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
Dbtux::TupLoc::setPageId(Uint32 pageId)
|
||||||
|
{
|
||||||
|
m_pageId1 = (pageId >> 16);
|
||||||
|
m_pageId2 = (pageId & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Uint32
|
||||||
|
Dbtux::TupLoc::getPageOffset() const
|
||||||
|
{
|
||||||
|
return (Uint32)m_pageOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
Dbtux::TupLoc::setPageOffset(Uint32 pageOffset)
|
||||||
|
{
|
||||||
|
m_pageOffset = (Uint16)pageOffset;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
Dbtux::TupLoc::operator==(const TupLoc& loc) const
|
Dbtux::TupLoc::operator==(const TupLoc& loc) const
|
||||||
{
|
{
|
||||||
return m_pageId == loc.m_pageId && m_pageOffset == loc.m_pageOffset;
|
return
|
||||||
|
m_pageId1 == loc.m_pageId1 &&
|
||||||
|
m_pageId2 == loc.m_pageId2 &&
|
||||||
|
m_pageOffset == loc.m_pageOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
@@ -851,13 +885,13 @@ Dbtux::TreeEnt::eq(const TreeEnt ent) const
|
|||||||
inline int
|
inline int
|
||||||
Dbtux::TreeEnt::cmp(const TreeEnt ent) const
|
Dbtux::TreeEnt::cmp(const TreeEnt ent) const
|
||||||
{
|
{
|
||||||
if (m_tupLoc.m_pageId < ent.m_tupLoc.m_pageId)
|
if (m_tupLoc.getPageId() < ent.m_tupLoc.getPageId())
|
||||||
return -1;
|
return -1;
|
||||||
if (m_tupLoc.m_pageId > ent.m_tupLoc.m_pageId)
|
if (m_tupLoc.getPageId() > ent.m_tupLoc.getPageId())
|
||||||
return +1;
|
return +1;
|
||||||
if (m_tupLoc.m_pageOffset < ent.m_tupLoc.m_pageOffset)
|
if (m_tupLoc.getPageOffset() < ent.m_tupLoc.getPageOffset())
|
||||||
return -1;
|
return -1;
|
||||||
if (m_tupLoc.m_pageOffset > ent.m_tupLoc.m_pageOffset)
|
if (m_tupLoc.getPageOffset() > ent.m_tupLoc.getPageOffset())
|
||||||
return +1;
|
return +1;
|
||||||
if (m_tupVersion < ent.m_tupVersion)
|
if (m_tupVersion < ent.m_tupVersion)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -880,12 +914,9 @@ Dbtux::TreeNode::TreeNode() :
|
|||||||
m_occup(0),
|
m_occup(0),
|
||||||
m_nodeScan(RNIL)
|
m_nodeScan(RNIL)
|
||||||
{
|
{
|
||||||
m_linkPI[0] = NullTupLoc.m_pageId;
|
m_link[0] = NullTupLoc;
|
||||||
m_linkPO[0] = NullTupLoc.m_pageOffset;
|
m_link[1] = NullTupLoc;
|
||||||
m_linkPI[1] = NullTupLoc.m_pageId;
|
m_link[2] = NullTupLoc;
|
||||||
m_linkPO[1] = NullTupLoc.m_pageOffset;
|
|
||||||
m_linkPI[2] = NullTupLoc.m_pageId;
|
|
||||||
m_linkPO[2] = NullTupLoc.m_pageOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dbtux::TreeHead
|
// Dbtux::TreeHead
|
||||||
@@ -913,7 +944,6 @@ Dbtux::TreeHead::getSize(AccSize acc) const
|
|||||||
case AccFull:
|
case AccFull:
|
||||||
return m_nodeSize;
|
return m_nodeSize;
|
||||||
}
|
}
|
||||||
abort();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,13 +1118,13 @@ inline Dbtux::TupLoc
|
|||||||
Dbtux::NodeHandle::getLink(unsigned i)
|
Dbtux::NodeHandle::getLink(unsigned i)
|
||||||
{
|
{
|
||||||
ndbrequire(i <= 2);
|
ndbrequire(i <= 2);
|
||||||
return TupLoc(m_node->m_linkPI[i], m_node->m_linkPO[i]);
|
return m_node->m_link[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
inline unsigned
|
inline unsigned
|
||||||
Dbtux::NodeHandle::getChilds()
|
Dbtux::NodeHandle::getChilds()
|
||||||
{
|
{
|
||||||
return (getLink(0) != NullTupLoc) + (getLink(1) != NullTupLoc);
|
return (m_node->m_link[0] != NullTupLoc) + (m_node->m_link[1] != NullTupLoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline unsigned
|
inline unsigned
|
||||||
@@ -1125,8 +1155,7 @@ inline void
|
|||||||
Dbtux::NodeHandle::setLink(unsigned i, TupLoc loc)
|
Dbtux::NodeHandle::setLink(unsigned i, TupLoc loc)
|
||||||
{
|
{
|
||||||
ndbrequire(i <= 2);
|
ndbrequire(i <= 2);
|
||||||
m_node->m_linkPI[i] = loc.m_pageId;
|
m_node->m_link[i] = loc;
|
||||||
m_node->m_linkPO[i] = loc.m_pageOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
@@ -1224,7 +1253,7 @@ Dbtux::getTupAddr(const Frag& frag, TreeEnt ent)
|
|||||||
const Uint32 tableFragPtrI = frag.m_tupTableFragPtrI[ent.m_fragBit];
|
const Uint32 tableFragPtrI = frag.m_tupTableFragPtrI[ent.m_fragBit];
|
||||||
const TupLoc tupLoc = ent.m_tupLoc;
|
const TupLoc tupLoc = ent.m_tupLoc;
|
||||||
Uint32 tupAddr = NullTupAddr;
|
Uint32 tupAddr = NullTupAddr;
|
||||||
c_tup->tuxGetTupAddr(tableFragPtrI, tupLoc.m_pageId, tupLoc.m_pageOffset, tupAddr);
|
c_tup->tuxGetTupAddr(tableFragPtrI, tupLoc.getPageId(), tupLoc.getPageOffset(), tupAddr);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
return tupAddr;
|
return tupAddr;
|
||||||
}
|
}
|
||||||
|
@@ -256,8 +256,8 @@ operator<<(NdbOut& out, const Dbtux::TupLoc& loc)
|
|||||||
if (loc == Dbtux::NullTupLoc) {
|
if (loc == Dbtux::NullTupLoc) {
|
||||||
out << "null";
|
out << "null";
|
||||||
} else {
|
} else {
|
||||||
out << dec << loc.m_pageId;
|
out << dec << loc.getPageId();
|
||||||
out << "." << dec << loc.m_pageOffset;
|
out << "." << dec << loc.getPageOffset();
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -274,13 +274,10 @@ operator<<(NdbOut& out, const Dbtux::TreeEnt& ent)
|
|||||||
NdbOut&
|
NdbOut&
|
||||||
operator<<(NdbOut& out, const Dbtux::TreeNode& node)
|
operator<<(NdbOut& out, const Dbtux::TreeNode& node)
|
||||||
{
|
{
|
||||||
Dbtux::TupLoc link0(node.m_linkPI[0], node.m_linkPO[0]);
|
|
||||||
Dbtux::TupLoc link1(node.m_linkPI[1], node.m_linkPO[1]);
|
|
||||||
Dbtux::TupLoc link2(node.m_linkPI[2], node.m_linkPO[2]);
|
|
||||||
out << "[TreeNode " << hex << &node;
|
out << "[TreeNode " << hex << &node;
|
||||||
out << " [left " << link0 << "]";
|
out << " [left " << node.m_link[0] << "]";
|
||||||
out << " [right " << link1 << "]";
|
out << " [right " << node.m_link[1] << "]";
|
||||||
out << " [up " << link2 << "]";
|
out << " [up " << node.m_link[2] << "]";
|
||||||
out << " [side " << dec << node.m_side << "]";
|
out << " [side " << dec << node.m_side << "]";
|
||||||
out << " [occup " << dec << node.m_occup << "]";
|
out << " [occup " << dec << node.m_occup << "]";
|
||||||
out << " [balance " << dec << (int)node.m_balance << "]";
|
out << " [balance " << dec << (int)node.m_balance << "]";
|
||||||
|
@@ -245,7 +245,7 @@ Dbtux::readKeyAttrs(const Frag& frag, TreeEnt ent, unsigned start, Data keyData)
|
|||||||
const Uint32 numAttrs = frag.m_numAttrs - start;
|
const Uint32 numAttrs = frag.m_numAttrs - start;
|
||||||
// skip to start position in keyAttrs only
|
// skip to start position in keyAttrs only
|
||||||
keyAttrs += start;
|
keyAttrs += start;
|
||||||
int ret = c_tup->tuxReadAttrs(tableFragPtrI, tupLoc.m_pageId, tupLoc.m_pageOffset, tupVersion, keyAttrs, numAttrs, keyData);
|
int ret = c_tup->tuxReadAttrs(tableFragPtrI, tupLoc.getPageId(), tupLoc.getPageOffset(), tupVersion, keyAttrs, numAttrs, keyData);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
// TODO handle error
|
// TODO handle error
|
||||||
ndbrequire(ret > 0);
|
ndbrequire(ret > 0);
|
||||||
@@ -256,7 +256,7 @@ Dbtux::readTablePk(const Frag& frag, TreeEnt ent, Data pkData, unsigned& pkSize)
|
|||||||
{
|
{
|
||||||
const Uint32 tableFragPtrI = frag.m_tupTableFragPtrI[ent.m_fragBit];
|
const Uint32 tableFragPtrI = frag.m_tupTableFragPtrI[ent.m_fragBit];
|
||||||
const TupLoc tupLoc = ent.m_tupLoc;
|
const TupLoc tupLoc = ent.m_tupLoc;
|
||||||
int ret = c_tup->tuxReadPk(tableFragPtrI, tupLoc.m_pageId, tupLoc.m_pageOffset, pkData);
|
int ret = c_tup->tuxReadPk(tableFragPtrI, tupLoc.getPageId(), tupLoc.getPageOffset(), pkData);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
// TODO handle error
|
// TODO handle error
|
||||||
ndbrequire(ret > 0);
|
ndbrequire(ret > 0);
|
||||||
|
@@ -235,6 +235,20 @@ Dbtux::execTUX_ADD_ATTRREQ(Signal* signal)
|
|||||||
tree.m_minOccup = tree.m_maxOccup - maxSlack;
|
tree.m_minOccup = tree.m_maxOccup - maxSlack;
|
||||||
// root node does not exist (also set by ctor)
|
// root node does not exist (also set by ctor)
|
||||||
tree.m_root = NullTupLoc;
|
tree.m_root = NullTupLoc;
|
||||||
|
#ifdef VM_TRACE
|
||||||
|
if (debugFlags & DebugMeta) {
|
||||||
|
if (fragOpPtr.p->m_fragNo == 0) {
|
||||||
|
debugOut << "Index id=" << indexPtr.i;
|
||||||
|
debugOut << " nodeSize=" << tree.m_nodeSize;
|
||||||
|
debugOut << " headSize=" << NodeHeadSize;
|
||||||
|
debugOut << " prefSize=" << tree.m_prefSize;
|
||||||
|
debugOut << " entrySize=" << TreeEntSize;
|
||||||
|
debugOut << " minOccup=" << tree.m_minOccup;
|
||||||
|
debugOut << " maxOccup=" << tree.m_maxOccup;
|
||||||
|
debugOut << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
// fragment is defined
|
// fragment is defined
|
||||||
c_fragOpPool.release(fragOpPtr);
|
c_fragOpPool.release(fragOpPtr);
|
||||||
}
|
}
|
||||||
|
@@ -24,8 +24,8 @@ int
|
|||||||
Dbtux::allocNode(Signal* signal, NodeHandle& node)
|
Dbtux::allocNode(Signal* signal, NodeHandle& node)
|
||||||
{
|
{
|
||||||
Frag& frag = node.m_frag;
|
Frag& frag = node.m_frag;
|
||||||
Uint32 pageId = NullTupLoc.m_pageId;
|
Uint32 pageId = NullTupLoc.getPageId();
|
||||||
Uint32 pageOffset = NullTupLoc.m_pageOffset;
|
Uint32 pageOffset = NullTupLoc.getPageOffset();
|
||||||
Uint32* node32 = 0;
|
Uint32* node32 = 0;
|
||||||
int errorCode = c_tup->tuxAllocNode(signal, frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
int errorCode = c_tup->tuxAllocNode(signal, frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
@@ -60,8 +60,8 @@ Dbtux::selectNode(Signal* signal, NodeHandle& node, TupLoc loc, AccSize acc)
|
|||||||
{
|
{
|
||||||
Frag& frag = node.m_frag;
|
Frag& frag = node.m_frag;
|
||||||
ndbrequire(loc != NullTupLoc);
|
ndbrequire(loc != NullTupLoc);
|
||||||
Uint32 pageId = loc.m_pageId;
|
Uint32 pageId = loc.getPageId();
|
||||||
Uint32 pageOffset = loc.m_pageOffset;
|
Uint32 pageOffset = loc.getPageOffset();
|
||||||
Uint32* node32 = 0;
|
Uint32* node32 = 0;
|
||||||
c_tup->tuxGetNode(frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
c_tup->tuxGetNode(frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
@@ -100,8 +100,8 @@ Dbtux::deleteNode(Signal* signal, NodeHandle& node)
|
|||||||
Frag& frag = node.m_frag;
|
Frag& frag = node.m_frag;
|
||||||
ndbrequire(node.getOccup() == 0);
|
ndbrequire(node.getOccup() == 0);
|
||||||
TupLoc loc = node.m_loc;
|
TupLoc loc = node.m_loc;
|
||||||
Uint32 pageId = loc.m_pageId;
|
Uint32 pageId = loc.getPageId();
|
||||||
Uint32 pageOffset = loc.m_pageOffset;
|
Uint32 pageOffset = loc.getPageOffset();
|
||||||
Uint32* node32 = reinterpret_cast<Uint32*>(node.m_node);
|
Uint32* node32 = reinterpret_cast<Uint32*>(node.m_node);
|
||||||
c_tup->tuxFreeNode(signal, frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
c_tup->tuxFreeNode(signal, frag.m_tupIndexFragPtrI, pageId, pageOffset, node32);
|
||||||
jamEntry();
|
jamEntry();
|
||||||
|
@@ -108,4 +108,11 @@ charsets mc02/a 35 ms 60 ms 71 pct
|
|||||||
|
|
||||||
[ case b: TUX can no longer use pointers to TUP data ]
|
[ case b: TUX can no longer use pointers to TUP data ]
|
||||||
|
|
||||||
|
optim 15 mc02/a 34 ms 60 ms 72 pct
|
||||||
|
mc02/b 42 ms 85 ms 100 pct
|
||||||
|
mc02/c 5 ms 12 ms 110 pct
|
||||||
|
mc02/d 178 ms 242 ms 35 pct
|
||||||
|
|
||||||
|
[ corrected wasted space in index node ]
|
||||||
|
|
||||||
vim: set et:
|
vim: set et:
|
||||||
|
@@ -212,6 +212,8 @@ struct Par : public Opt {
|
|||||||
// value calculation
|
// value calculation
|
||||||
unsigned m_range;
|
unsigned m_range;
|
||||||
unsigned m_pctrange;
|
unsigned m_pctrange;
|
||||||
|
// choice of key
|
||||||
|
bool m_randomkey;
|
||||||
// do verify after read
|
// do verify after read
|
||||||
bool m_verify;
|
bool m_verify;
|
||||||
// deadlock possible
|
// deadlock possible
|
||||||
@@ -227,6 +229,7 @@ struct Par : public Opt {
|
|||||||
m_totrows(m_threads * m_rows),
|
m_totrows(m_threads * m_rows),
|
||||||
m_range(m_rows),
|
m_range(m_rows),
|
||||||
m_pctrange(0),
|
m_pctrange(0),
|
||||||
|
m_randomkey(false),
|
||||||
m_verify(false),
|
m_verify(false),
|
||||||
m_deadlock(false) {
|
m_deadlock(false) {
|
||||||
}
|
}
|
||||||
@@ -2119,7 +2122,8 @@ pkupdate(Par par)
|
|||||||
Lst lst;
|
Lst lst;
|
||||||
bool deadlock = false;
|
bool deadlock = false;
|
||||||
for (unsigned j = 0; j < par.m_rows; j++) {
|
for (unsigned j = 0; j < par.m_rows; j++) {
|
||||||
unsigned i = thrrow(par, j);
|
unsigned j2 = ! par.m_randomkey ? j : urandom(par.m_rows);
|
||||||
|
unsigned i = thrrow(par, j2);
|
||||||
set.lock();
|
set.lock();
|
||||||
if (! set.exist(i) || set.pending(i)) {
|
if (! set.exist(i) || set.pending(i)) {
|
||||||
set.unlock();
|
set.unlock();
|
||||||
@@ -2722,6 +2726,7 @@ pkupdateindexbuild(Par par)
|
|||||||
if (par.m_no == 0) {
|
if (par.m_no == 0) {
|
||||||
CHK(createindex(par) == 0);
|
CHK(createindex(par) == 0);
|
||||||
} else {
|
} else {
|
||||||
|
par.m_randomkey = true;
|
||||||
CHK(pkupdate(par) == 0);
|
CHK(pkupdate(par) == 0);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
Reference in New Issue
Block a user