Added user-level documentation for tcharm, the basis of AMPI, FEM, MBLOCK, ...
authorOrion Lawlor <olawlor@acm.org>
Tue, 26 Mar 2002 22:54:16 +0000 (22:54 +0000)
committerOrion Lawlor <olawlor@acm.org>
Tue, 26 Mar 2002 22:54:16 +0000 (22:54 +0000)
doc/tcharm/Makefile [new file with mode: 0644]
doc/tcharm/manual.tex [new file with mode: 0644]

diff --git a/doc/tcharm/Makefile b/doc/tcharm/Makefile
new file mode 100644 (file)
index 0000000..07038ac
--- /dev/null
@@ -0,0 +1,50 @@
+DOCDIR=../../../doc
+WEBDIR=/expand6/groupMosaic/ppl_manuals
+BASE=tcharm
+
+all: ps pdf html
+
+ps:
+       touch index.tex
+       latex manual.tex
+       latex manual.tex
+       if [ -f manual.idx ] ; then makeindex -o index.tex manual.idx ; fi
+       latex manual.tex
+       dvips -o manual.ps manual.dvi
+
+html:
+       touch index.tex
+       -@ln -s ../pplmanual.* .
+       latex manual.tex
+       latex2html -local_icons manual.tex
+
+pdf:
+       touch index.tex
+       pdflatex manual.tex
+       if [ -f manual.idx ] ; then makeindex -o index.tex manual.idx ; fi
+       pdflatex manual.tex
+
+doc:
+       make all
+       if [ ! -d $(DOCDIR) ] ; then mkdir $(DOCDIR) ; fi
+       if [ ! -d $(DOCDIR)/ps ] ; then mkdir $(DOCDIR)/ps ; fi
+       if [ ! -d $(DOCDIR)/pdf ] ; then mkdir $(DOCDIR)/pdf ; fi
+       if [ ! -d $(DOCDIR)/html ] ; then mkdir $(DOCDIR)/html ; fi
+       /bin/cp manual.ps $(DOCDIR)/ps/fem.ps
+       /bin/cp manual.pdf $(DOCDIR)/pdf/fem.pdf
+       /bin/rm -rf $(DOCDIR)/html/fem
+       /bin/cp -R manual $(DOCDIR)/html/fem
+
+web:
+       make all
+       /bin/cp manual.ps $(WEBDIR)/ps/$(BASE).ps
+       /bin/cp manual.pdf $(WEBDIR)/pdf/$(BASE).pdf
+       /bin/rm -rf $(WEBDIR)/html/$(BASE)
+       /bin/cp -R manual $(WEBDIR)/html/$(BASE)
+       find $(WEBDIR) -type f -exec chmod 664 {} \;
+       find $(WEBDIR) -type d -exec chmod 775 {} \;
+
+clean:
+       /bin/rm -f *.ps *.pdf *.ilg *.aux *.log *.dvi *.idx *.toc 
+       /bin/rm -f index.tex *.blg *.bbl
+       /bin/rm -rf manual
diff --git a/doc/tcharm/manual.tex b/doc/tcharm/manual.tex
new file mode 100644 (file)
index 0000000..1e970bd
--- /dev/null
@@ -0,0 +1,566 @@
+\documentclass[10pt]{article}
+\usepackage{../pplmanual}
+\input{../pplmanual}
+
+\newcommand{\tcharm}{\textsc{TCharm}}
+
+\makeindex
+
+\title{Threaded \charmpp Manual}
+\version{1.0}
+\credits{
+The initial version of Threaded \charmpp{} was developed
+in late 2001 by Orion Lawlor.
+}
+
+\begin{document}
+
+\maketitle
+
+\section{Motivation}
+
+\charmpp{} includes several application frameworks, such as the 
+Finite Element Framework, the Multiblock Framework, and AMPI.  
+These frameworks do almost all their work in load balanced, 
+migratable threads.  
+
+The Threaded \charmpp{} Framework, \tcharm{}, provides both
+common runtime support for these threads and facilities for
+combining multiple frameworks within a single program.
+For example, you can use \tcharm{} to create a Finite Element
+Framework application that also uses AMPI to communicate between
+Finite Element chunks.
+
+Specifically, \tcharm{} provides language-neutral interfaces for:
+\begin{enumerate}
+\item{Program startup, including read-only global data setup and the
+configuration of multiple frameworks.}
+\item{Run-time load balancing, including migration.}
+\item{Program shutdown.}
+\end{enumerate}
+
+The first portion of this manual describes the general properties 
+of \tcharm{} common to all the application frameworks, such as 
+program contexts and how to write migratable code.
+The second portion describes in detail how to combine separate 
+frameworks into a single application.
+
+
+\section{Basic \tcharm{} Programming}
+Any routine in a \tcharm{} program runs in one of two contexts:
+
+\begin{description}
+
+\item[Serial Context] Routines that run on only one processor
+and with only one set of data. There are absolutely
+no limitations on what a serial context routine can do---it 
+is as if the code were running in an ordinary serial program.
+Startup and shutdown routines usually run in the serial context.
+
+\item[Parallel Context] Routines that run on several processors,
+and may run with several different sets of data on a single processor.
+This kind of routine must obey certain restrictions.  The program's
+main computation routines always run in the parallel context.
+
+\end{description}
+
+Parallel context routines run in a migratable, 
+user-level thread maintained by \tcharm{}.  
+Since there are normally several of these threads per processor,
+any code that runs in the parallel context has to be thread-safe.
+However, \tcharm{} 
+is non-preemptive, so it will only switch threads when you make
+a blocking call, like ``MPI\_Recv" or ``FEM\_Update\_Field".
+
+
+
+\subsection{Global Variables}
+\label{sec:global}
+By ``global variables'', we mean anything that is stored at a fixed, 
+preallocated location in memory.  In C, this means variables declared 
+at file scope or with the \kw{static} keyword.  In Fortran, this is
+variables that are part of a \kw{COMMON} block or declared inside 
+a \kw{MODULE}.
+
+Global variables are shared by all the threads on a processor, which
+makes using global variables extremely error prone.
+To see why this is a problem, consider a program fragment like:
+
+\begin{alltt}
+  foo=a
+  call MPI_Recv(...)
+  b=foo
+\end{alltt}
+
+After this code executes, we might expect \uw{b} to always be equal to \uw{a}.
+But if \uw{foo} is a global variable, \kw{MPI\_Recv} may block and 
+\uw{foo} could be changed by another thread.
+
+For example, if two threads execute this program, they could interleave like:
+
+\vspace{0.1in}
+\begin{tabular}{|l|l|}\hline
+\em{Thread 1} & \em{Thread 2}\\
+\hline
+\uw{foo}=1 & \\
+block in MPI\_Recv & \\
+ & \uw{foo}=2 \\
+ & block in MPI\_Recv \\
+\uw{b}=\uw{foo} & \\
+\hline\end{tabular}
+\vspace{0.1in}
+
+At this point, thread 1 might expect \uw{b} to be 1; but it will actually be 2.
+From the point of view of thread 1, the global variable \uw{foo} suddenly
+changed its value during the call to \kw{MPI\_Recv}.
+
+There are several possible solutions to this problem:
+
+\begin{itemize}
+\item Never use global variables---only use parameters or local variables.  
+This is the safest and most general solution.
+One standard practice is to collect all the globals into a C struct or 
+Fortran type named ``Globals", and pass a pointer to this object to all
+your subroutines.  This also combines well with the pup method for doing
+migration-based load balancing, as described in Section~\ref{sec:pup}.
+
+\item Never write {\em different} values to global variables.  If every thread
+writes the same value, global variables can be used safely.  For example,
+you might store some parameters read from a configuration file like the 
+simulation timestep $\Delta t$.  See Section~\ref{sec:readonlyglobal}
+for another, more convenient way to set such variables.
+
+\item Never issue a blocking call while your global variables are set.
+This will not work on a SMP version of Charm++, where several processors
+may share a single set of global variables.
+Even on a non-SMP version, this is a dangerous solution, because someday 
+someone might add a blocking call while the variables are set.  
+This is only a reasonable solution when calling legacy code or 
+using old serial libraries that might use global variables.
+\end{itemize}
+
+The above only applies to routines that run in the parallel context.
+There are no restrictions on global variables for serial context
+code.
+
+
+
+\subsection{Input/Output}
+\label{sec:io}
+
+In the parallel context, there are several limitations on open
+files.  First, several threads may run on one processor, so
+Fortran Logical Unit Numbers are shared by all the threads on
+a processor.  Second, open files are left behind when a thread 
+migrates to another processor---it is a crashing error to open a 
+file, migrate, then try to read from the file.
+
+Because of these restrictions, it is best to open files only when
+needed, and close them as soon as possible.  In particular, it
+is best if there are no open files whenever you make blocking calls.
+
+
+\subsection{Migration-Based Load Balancing}
+\label{sec:migration}
+\label{sec:isomalloc}
+
+The \charmpp\ runtime framework includes an automatic run-time load balancer,
+which can monitor the performance of your parallel program.
+If needed, the load balancer can ``migrate'' threads from heavily-loaded
+processors to more lightly-loaded processors, improving the load balance and
+speeding up the program.  For this to be useful, you need to pass the 
+link-time argument \kw{-balancer} \uw{B} to set the load balancing algorithm,
+and the run-time argument \kw{+vp} \uw{N} (use \uw{N} virtual processors)
+to set the number of threads.
+The ideal number of threads per processor depends on the problem, but
+we've found five to a hundred threads per processor to be a useful range.
+
+When a thread migrates, all its data must be brought with it.
+``Stack data'', such as variables declared locally in a subroutine,
+will be brought along with the thread automatically.  Global data,
+as described in Section~\ref{sec:global}, is never brought with the thread
+and should generally be avoided.
+
+``Heap data'' in C is structures and arrays allocated using \kw{malloc} or \kw{new};
+in Fortran, heap data is TYPEs or arrays allocated using \kw{ALLOCATE}.
+To bring heap data along with a migrating thread, you have two choices:
+write a pup routine or use isomalloc.  Pup routines are described in 
+Section~\ref{sec:pup}.
+
+{\em Isomalloc} is a special mode which controls the allocation of heap data.  
+You enable isomalloc allocation using the link-time flag ``-memory isomalloc''.  
+With isomalloc, migration is completely transparent---all your allocated data 
+is automatically brought to the new processor.  The data will be unpacked at the same
+location (the same virtual addresses) as it was stored originally; so even
+cross-linked data structures that contain pointers still work properly.
+
+The limitations of isomalloc are:
+\begin{itemize}
+\item Wasted memory.  Isomalloc uses a special interface\footnote{
+The interface used is \kw{mmap}.} to aquire memory, and the finest granularity
+that can be aquired is one page, typically 4KB.  This means if you allocate
+a 2-entry array, isomalloc will waste an entire 4KB page.  We should eventually 
+be able to reduce this overhead for small allocations.
+
+\item Limited space on 32-bit machines.  Machines where pointers are 32 bits
+long can address just 4GB ($2^32$ bytes) of virtual address space.  Additionally, 
+the operating system and conventional heap already use a significant amount 
+of this space; so the total virtual address space available is typically under 1GB.  
+With isomalloc, all processors share this space, so with just 20 processors
+processors the amount of memory per processor is limited to under 50MB!  This is an 
+inherent limitation of 32-bit machines; to run on more than a few processors you 
+must use 64-bit machines or avoid isomalloc.
+\end{itemize}
+
+
+
+\section{Advanced \tcharm{} Programming}
+The preceeding features are enough to write simple programs
+that use \tcharm{}-based frameworks.  These more advanced techniques
+provide the user with additional capabilities or flexibility.
+
+
+\subsection{Writing a Pup Routine}
+\label{sec:pup}
+
+The runtime system can automatically move your thread stack to the new
+processor, but unless you use isomalloc, you must write a pup routine to 
+move any global or heap-allocated data to the new processor.  A pup
+(Pack/UnPack) routine can perform both packing (converting your data into a
+network message) and unpacking (converting the message back into your data).  
+A pup routine is passed a pointer to your data block and a
+special handle called a ``pupper'', which contains the network message.  
+
+In a pup routine, you pass all your heap data to routines named \kw{pup\_}\uw{type} or \kw{fpup\_}\uw{type}, where
+\uw{type} is either a basic type (such as int, char, float, or double) or an array
+type (as before, but with a ``s'' suffix).  Depending on the direction of
+packing, the pupper will either read from or write to the values you pass--
+normally, you shouldn't even know which.  The only time you need to know the
+direction is when you are leaving a processor, or just arriving.
+Correspondingly, the pupper passed to you may be deleting (indicating that you
+are leaving the processor, and should delete your heap storage after packing),
+unpacking (indicating you've just arrived on a processor, and should allocate
+your heap storage before unpacking), or neither (indicating the system is
+merely sizing a buffer, or checkpointing your values).
+
+pup functions are much easier to write than explain-- a simple C heap block
+and the corresponding pup function is:
+
+\begin{alltt}
+     typedef struct \{
+       int n1;/*Length of first array below*/
+       int n2;/*Length of second array below*/
+       double *arr1; /*Some doubles, allocated on the heap*/
+       int *arr2; /*Some ints, allocated on the heap*/
+     \} my_block;
+     void pup_my_block(pup_er p,my_block *m)
+     \{
+       if (pup_isUnpacking(p)) \{ /*Arriving on new processor*/
+         m->arr1=malloc(m->n1*sizeof(double));
+         m->arr2=malloc(m->n2*sizeof(int));
+       \}
+       pup_doubles(p,m->arr1,m->n1);
+       pup_ints(p,m->arr2,m->n2);
+       if (pup_isDeleting(p)) \{ /*Leaving old processor*/
+         free(m->arr1);
+         free(m->arr2);
+       \}
+     \}
+\end{alltt}
+
+This single pup function can be used to copy the \kw{my\_block} data into a
+message buffer and free the old heap storage (deleting pupper); allocate
+storage on the new processor and copy the message data back (unpacking pupper);
+or save the heap data for debugging or checkpointing.
+
+A Fortran block TYPE and corresponding pup routine is as follows:
+
+\begin{alltt}
+     MODULE my_block_mod
+       TYPE my_block
+         INTEGER :: n1,n2x,n2y
+         DOUBLE PRECISION, ALLOCATABLE, DIMENSION(:) :: arr1
+         INTEGER, ALLOCATABLE, DIMENSION(:,:) :: arr2
+       END TYPE
+     END MODULE
+     SUBROUTINE pup_my_block(p,m)
+       IMPLICIT NONE
+       USE my_block_mod
+       USE pupmod
+       INTEGER :: p
+       TYPE(my_block) :: m
+       IF (fpup_isUnpacking(p)) THEN
+         ALLOCATE(m%arr1(m%n1))
+         ALLOCATE(m%arr2(m%n2x,m%n2y))
+       END IF
+       call fpup_doubles(p,m%arr1,m%n1)
+       call fpup_ints(p,m%arr2,m%n2x*m%n2y)
+       IF (fpup_isDeleting(p)) THEN
+         DEALLOCATE(m%arr1)
+         DEALLOCATE(m%arr2)
+       END IF
+     END SUBROUTINE
+\end{alltt}
+
+
+You indicate to \tcharm{} that you want a pup routine called using
+the routine below.  An arbitrary number of blocks can be registered
+in this fashion.
+
+\vspace{0.2in}
+\function{void TCharmRegister(void *block, TCharmPupFn pup\_fn)}
+\function{SUBROUTINE tcharm\_register(block,pup\_fn)}
+    \args{TYPE(varies), POINTER :: block}
+    \args{SUBROUTINE :: pup\_fn}
+
+     Associate the given data block and pup function.  Can only be
+     called from the parallel context.  For the declarations above, you call
+     \kw{TCharmRegister} as:
+
+\begin{alltt}
+          /*In C/C++ driver() function*/
+          my_block m;
+          TCharmRegister(m,(TCharmPupFn)pup_my_block);
+          !- In Fortran driver subroutine
+          use my_block_mod
+          interface
+            subroutine pup_my_block(p,m)
+              use my_block_mod
+              INTEGER :: p
+              TYPE(my_block) :: m
+            end subroutine
+          end interface
+          TYPE(my_block) :: m
+          call TCharmRegister(m,pup_my_block)
+\end{alltt}
+
+     Note that the data block must be allocated on the stack.
+
+\vspace{0.2in}
+\function{void TCharmMigrate()}
+\function{subroutine TCharmMigrate()}
+
+     Informs the load balancing system that you are ready to be
+     migrated, if needed.  If the system decides to migrate you, the
+     pup function passed to \kw{TCharm\_Register} will first be called with 
+     a sizing pupper, then a packing, deleting pupper.  Your stack and pupped
+     data will then be sent to the destination machine, where your pup
+     function will be called with an unpacking pupper.  \kw{TCharm\_Migrate}
+     will then return.  Can only be called from in the parallel context.
+
+
+
+\subsection{Readonly Global Variables}
+\label{sec:readonlyglobal}
+
+You can also use a pup routine to set up initial values for global
+variables on all processors.  This pup routine is called with only
+a pup handle, just after the serial setup routine, and just before 
+any parallel context routines start.  The pup routine is never
+called with a deleting pup handle, so you need not handle that case.
+
+A C example is:
+\begin{alltt}
+     int g_arr[17];
+     double g_f;
+     int g_n; /*Length of array below*/
+     float *g_allocated; /*heap-allocated array*/
+     void pup_my_globals(pup_er p)
+     \{
+       pup_ints(p,g_arr,17);
+       pup_double(p,&g_f);
+       pup_int(p,&g_n);
+       if (pup_isUnpacking(p)) \{ /*Arriving on new processor*/
+         g_allocated=malloc(g_n*sizeof(float));
+       \}
+       pup_floats(p,g_allocated,g_n);
+     \}
+\end{alltt}
+
+A fortran example is:
+\begin{alltt}
+     MODULE my_globals_mod
+       INTEGER :: g_arr(17)
+       DOUBLE PRECISION :: g_f
+       INTEGER :: g_n
+       SINGLE PRECISION, ALLOCATABLE :: g_allocated(:)
+     END MODULE
+     SUBROUTINE pup_my_globals(p)
+       IMPLICIT NONE
+       USE my_globals_mod
+       USE pupmod
+       INTEGER :: p
+       call fpup_ints(p,g_arr,17)
+       call fpup_double(p,g_f)
+       call fpup_int(p,g_n)
+       IF (fpup_isUnpacking(p)) THEN
+         ALLOCATE(g_allocated(g_n))
+       END IF
+       call fpup_floats(p,g_allocated,g_n)
+     END SUBROUTINE
+\end{alltt}
+
+
+You register your global variable pup routine using the method below.
+Multiple pup routines can be registered the same way.
+
+\vspace{0.2in}
+\function{void TCharmReadonlyGlobals(TCharmPupGlobalFn pup\_fn)}
+\function{SUBROUTINE tcharm\_readonly\_globals(pup\_fn)}
+    \args{SUBROUTINE :: pup\_fn}
+
+
+
+\section{Combining Frameworks}
+\label{sec:combining}
+
+This section describes how to combine multiple frameworks in a 
+single application.  You might want to do this, for example,
+to use AMPI communication inside a finite element method solver.
+
+You specify how you want the frameworks to be combined by writing
+a special setup routine that runs when the program starts.
+The setup routine must be named TCharmUserSetup if written in C, 
+or tcharm\_user\_setup if written in Fortran.  If you declare a 
+user setup routine, the standard framework setup routines (such
+as the FEM framework's \kw{init} routine) are bypassed, and you
+do all the setup in the user setup routine.
+
+The setup routine creates a set of threads and then attaches frameworks
+to the threads.  Several different frameworks can be attached to one thread set,
+and there can be several sets of threads; however, the most frameworks
+cannot be attached more than once to single set of threads. That is, a single
+thread cannot have two attached AMPI frameworks, since the MPI\_COMM\_WORLD 
+for such a thread would be indeterminate.
+
+\vspace{0.2in}
+\function{void TCharmCreateThreads(int nThreads, TCharmThreadStartFn thread\_fn)}
+\function{SUBROUTINE tcharm\_create\_threads(nThreads,thread\_fn)}
+    \args{INTEGER, INTENT(in) :: nThreads}
+    \args{SUBROUTINE :: thread\_fn}
+
+Create a new set of \tcharm{} threads of the given size.  The threads will
+execute the given function, which is normally your user code.  
+You should call \kw{TCharmGetNumChunks()} (in Fortran, 
+\kw{tcharm\_get\_num\_chunks()})
+to get the number of threads from the command line.  This routine can 
+only be called from your \kw{TCharmUserSetup} routine.
+
+You then attach frameworks to the new threads.  The order in which
+frameworks are attached is irrelevant, but attach commands always apply
+to the current set of threads.
+  
+The commands to attach the various frameworks are:
+
+\begin{description}
+\item[AMPI] You attach a new \kw{MPI\_COMM\_WORLD} to the current threads
+using the \kw{MPI\_Attach} routine, which takes a string, the name of the
+communicator.  These threads will then be able to use AMPI calls.
+
+\item[FEM Framework] You attach the current FEM mesh to the current threads
+using the \kw{FEM\_Attach} routine, which takes an integer which is normally
+zero.  These threads will then be able to use FEM calls.  Be sure to set up 
+an FEM mesh using the \kw{FEM\_Set} calls before your \kw{FEM\_Attach}.
+
+\item[Multiblock Framework] You attach the current multiblock mesh to the current threads
+using the \kw{MBLK\_Attach} routine, which takes no parameters.  
+These threads will then be able to use Multiblock calls.  Be sure to set up 
+the multiblock mesh using the \kw{MBLK\_Set} calls before your \kw{MBLK\_Attach}.
+
+\end{description}
+
+For example, the C code to start an application having one set of threads
+that performs both AMPI communication and FEM computations inside a 
+routine called \uw{myDriver} would be:
+
+\begin{alltt}
+     #include "tcharmc.h"
+     #include "ampi.h"
+     #include "fem.h"
+     
+     /* Called to start the program */
+     void TCharmUserSetup(void)
+     \{
+         TCharmCreateThreads(TCharmGetNumChunks(),myDriver);
+         MPI\_Attach("myAMPIFEM");        
+         ... Usual FEM_Set calls, as normally made from init() ...
+         FEM\_Attach(0);
+     \}
+\end{alltt}
+
+The Fortran code to start an application consisting of two sets of threads,
+both of which perform AMPI communication, would be:
+
+\begin{alltt}
+     SUBROUTINE tcharm\_user\_setup()
+       IMPLICIT NONE
+       include 'tcharmf.h'
+       include 'ampif.h'
+       
+    ! First set of threads, running "compute1"
+       call tcharm\_create\_threads(tcharm\_get\_num\_chunks(),compute1)
+       call ampi\_attach('part1')
+       
+    ! Second set of threads, running "compute2"
+       call tcharm\_create\_threads(tcharm\_get\_num\_chunks(),compute2)
+       call ampi\_attach('part2')
+     END SUBROUTINE
+\end{alltt}
+
+
+
+\section{Command-line Options}
+\label{sec:cla}
+
+The complete set of link-time arguments relevant to \tcharm{} is:
+\begin{description}
+\item[-memory isomalloc] Enable memory allocation that will automatically
+migrate with the thread, as described in Section~\ref{sec:isomalloc}.
+
+\item[-balancer \uw{B}] Enable this load balancing strategy.  The
+current set of balancers \uw{B} includes RefineLB (make only small changes
+each time), MetisLB (remap threads using graph partitioning library), 
+HeapCentLB (remap threads using a greedy algorithm), and RandCentLB
+(remap threads to random processors).  You can only have one balancer.
+
+\item[-module \uw{F}] Link in this framework.  The current set of frameworks
+\uw{F} includes ampi, collide, fem, mblock, and netfem.  You can link in 
+multiple frameworks.
+
+\end{description}
+
+The complete set of command-line arguments relevant to \tcharm{} is:
+
+\begin{description}
+\item[+p \uw{N}] Run on \uw{N} physical processors.
+
+\item[+vp \uw{N}] Create \uw{N} ``virtual processors'', or threads.  This is
+the value returned by TCharmGetNumChunks.
+
+\item[++debug] Start each program in a debugger window.  See Charm++
+Installation and Usage Manual for details.
+
+\item[+tcharm\_stacksize \uw{N}] Create \uw{N}-byte thread stacks.  This
+value can be overridden using TCharmSetStackSize().
+
+\item[+tcharm\_nomig] Disable thread migration.  This can help determine
+whether a problem you encounter is caused by our migration framework.
+
+\item[+tcharm\_nothread] Disable threads entirely.  This can help determine
+whether a problem you encounter is caused by our threading framework.
+This generally only works properly when using only one thread.
+
+\item[+tcharm\_trace \uw{F}] Trace all calls made to the framework \uw{F}.
+This can help to understand a complex program.  This feature is not
+available if \charmpp{} was compiled with CMK\_OPTIMIZE.
+
+\end{description}
+
+
+\input{index}
+\end{document}