...
Run Format

Source file src/runtime/pprof/proto.go

Documentation: runtime/pprof

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package pprof
     6  
     7  import (
     8  	"bytes"
     9  	"compress/gzip"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"runtime"
    14  	"strconv"
    15  	"time"
    16  	"unsafe"
    17  )
    18  
    19  // lostProfileEvent is the function to which lost profiling
    20  // events are attributed.
    21  // (The name shows up in the pprof graphs.)
    22  func lostProfileEvent() { lostProfileEvent() }
    23  
    24  // funcPC returns the PC for the func value f.
    25  func funcPC(f interface{}) uintptr {
    26  	return *(*[2]*uintptr)(unsafe.Pointer(&f))[1]
    27  }
    28  
    29  // A profileBuilder writes a profile incrementally from a
    30  // stream of profile samples delivered by the runtime.
    31  type profileBuilder struct {
    32  	start      time.Time
    33  	end        time.Time
    34  	havePeriod bool
    35  	period     int64
    36  	m          profMap
    37  
    38  	// encoding state
    39  	w         io.Writer
    40  	zw        *gzip.Writer
    41  	pb        protobuf
    42  	strings   []string
    43  	stringMap map[string]int
    44  	locs      map[uintptr]int
    45  	funcs     map[string]int // Package path-qualified function name to Function.ID
    46  	mem       []memMap
    47  }
    48  
    49  type memMap struct {
    50  	// initialized as reading mapping
    51  	start         uintptr
    52  	end           uintptr
    53  	offset        uint64
    54  	file, buildID string
    55  
    56  	funcs symbolizeFlag
    57  	fake  bool // map entry was faked; /proc/self/maps wasn't available
    58  }
    59  
    60  // symbolizeFlag keeps track of symbolization result.
    61  //   0                  : no symbol lookup was performed
    62  //   1<<0 (lookupTried) : symbol lookup was performed
    63  //   1<<1 (lookupFailed): symbol lookup was performed but failed
    64  type symbolizeFlag uint8
    65  
    66  const (
    67  	lookupTried  symbolizeFlag = 1 << iota
    68  	lookupFailed symbolizeFlag = 1 << iota
    69  )
    70  
    71  const (
    72  	// message Profile
    73  	tagProfile_SampleType        = 1  // repeated ValueType
    74  	tagProfile_Sample            = 2  // repeated Sample
    75  	tagProfile_Mapping           = 3  // repeated Mapping
    76  	tagProfile_Location          = 4  // repeated Location
    77  	tagProfile_Function          = 5  // repeated Function
    78  	tagProfile_StringTable       = 6  // repeated string
    79  	tagProfile_DropFrames        = 7  // int64 (string table index)
    80  	tagProfile_KeepFrames        = 8  // int64 (string table index)
    81  	tagProfile_TimeNanos         = 9  // int64
    82  	tagProfile_DurationNanos     = 10 // int64
    83  	tagProfile_PeriodType        = 11 // ValueType (really optional string???)
    84  	tagProfile_Period            = 12 // int64
    85  	tagProfile_Comment           = 13 // repeated int64
    86  	tagProfile_DefaultSampleType = 14 // int64
    87  
    88  	// message ValueType
    89  	tagValueType_Type = 1 // int64 (string table index)
    90  	tagValueType_Unit = 2 // int64 (string table index)
    91  
    92  	// message Sample
    93  	tagSample_Location = 1 // repeated uint64
    94  	tagSample_Value    = 2 // repeated int64
    95  	tagSample_Label    = 3 // repeated Label
    96  
    97  	// message Label
    98  	tagLabel_Key = 1 // int64 (string table index)
    99  	tagLabel_Str = 2 // int64 (string table index)
   100  	tagLabel_Num = 3 // int64
   101  
   102  	// message Mapping
   103  	tagMapping_ID              = 1  // uint64
   104  	tagMapping_Start           = 2  // uint64
   105  	tagMapping_Limit           = 3  // uint64
   106  	tagMapping_Offset          = 4  // uint64
   107  	tagMapping_Filename        = 5  // int64 (string table index)
   108  	tagMapping_BuildID         = 6  // int64 (string table index)
   109  	tagMapping_HasFunctions    = 7  // bool
   110  	tagMapping_HasFilenames    = 8  // bool
   111  	tagMapping_HasLineNumbers  = 9  // bool
   112  	tagMapping_HasInlineFrames = 10 // bool
   113  
   114  	// message Location
   115  	tagLocation_ID        = 1 // uint64
   116  	tagLocation_MappingID = 2 // uint64
   117  	tagLocation_Address   = 3 // uint64
   118  	tagLocation_Line      = 4 // repeated Line
   119  
   120  	// message Line
   121  	tagLine_FunctionID = 1 // uint64
   122  	tagLine_Line       = 2 // int64
   123  
   124  	// message Function
   125  	tagFunction_ID         = 1 // uint64
   126  	tagFunction_Name       = 2 // int64 (string table index)
   127  	tagFunction_SystemName = 3 // int64 (string table index)
   128  	tagFunction_Filename   = 4 // int64 (string table index)
   129  	tagFunction_StartLine  = 5 // int64
   130  )
   131  
   132  // stringIndex adds s to the string table if not already present
   133  // and returns the index of s in the string table.
   134  func (b *profileBuilder) stringIndex(s string) int64 {
   135  	id, ok := b.stringMap[s]
   136  	if !ok {
   137  		id = len(b.strings)
   138  		b.strings = append(b.strings, s)
   139  		b.stringMap[s] = id
   140  	}
   141  	return int64(id)
   142  }
   143  
   144  func (b *profileBuilder) flush() {
   145  	const dataFlush = 4096
   146  	if b.pb.nest == 0 && len(b.pb.data) > dataFlush {
   147  		b.zw.Write(b.pb.data)
   148  		b.pb.data = b.pb.data[:0]
   149  	}
   150  }
   151  
   152  // pbValueType encodes a ValueType message to b.pb.
   153  func (b *profileBuilder) pbValueType(tag int, typ, unit string) {
   154  	start := b.pb.startMessage()
   155  	b.pb.int64(tagValueType_Type, b.stringIndex(typ))
   156  	b.pb.int64(tagValueType_Unit, b.stringIndex(unit))
   157  	b.pb.endMessage(tag, start)
   158  }
   159  
   160  // pbSample encodes a Sample message to b.pb.
   161  func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) {
   162  	start := b.pb.startMessage()
   163  	b.pb.int64s(tagSample_Value, values)
   164  	b.pb.uint64s(tagSample_Location, locs)
   165  	if labels != nil {
   166  		labels()
   167  	}
   168  	b.pb.endMessage(tagProfile_Sample, start)
   169  	b.flush()
   170  }
   171  
   172  // pbLabel encodes a Label message to b.pb.
   173  func (b *profileBuilder) pbLabel(tag int, key, str string, num int64) {
   174  	start := b.pb.startMessage()
   175  	b.pb.int64Opt(tagLabel_Key, b.stringIndex(key))
   176  	b.pb.int64Opt(tagLabel_Str, b.stringIndex(str))
   177  	b.pb.int64Opt(tagLabel_Num, num)
   178  	b.pb.endMessage(tag, start)
   179  }
   180  
   181  // pbLine encodes a Line message to b.pb.
   182  func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
   183  	start := b.pb.startMessage()
   184  	b.pb.uint64Opt(tagLine_FunctionID, funcID)
   185  	b.pb.int64Opt(tagLine_Line, line)
   186  	b.pb.endMessage(tag, start)
   187  }
   188  
   189  // pbMapping encodes a Mapping message to b.pb.
   190  func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file, buildID string, hasFuncs bool) {
   191  	start := b.pb.startMessage()
   192  	b.pb.uint64Opt(tagMapping_ID, id)
   193  	b.pb.uint64Opt(tagMapping_Start, base)
   194  	b.pb.uint64Opt(tagMapping_Limit, limit)
   195  	b.pb.uint64Opt(tagMapping_Offset, offset)
   196  	b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
   197  	b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
   198  	// TODO: we set HasFunctions if all symbols from samples were symbolized (hasFuncs).
   199  	// Decide what to do about HasInlineFrames and HasLineNumbers.
   200  	// Also, another approach to handle the mapping entry with
   201  	// incomplete symbolization results is to dupliace the mapping
   202  	// entry (but with different Has* fields values) and use
   203  	// different entries for symbolized locations and unsymbolized locations.
   204  	if hasFuncs {
   205  		b.pb.bool(tagMapping_HasFunctions, true)
   206  	}
   207  	b.pb.endMessage(tag, start)
   208  }
   209  
   210  // locForPC returns the location ID for addr.
   211  // addr must be a return PC. This returns the location of the call.
   212  // It may emit to b.pb, so there must be no message encoding in progress.
   213  func (b *profileBuilder) locForPC(addr uintptr) uint64 {
   214  	id := uint64(b.locs[addr])
   215  	if id != 0 {
   216  		return id
   217  	}
   218  
   219  	// Expand this one address using CallersFrames so we can cache
   220  	// each expansion. In general, CallersFrames takes a whole
   221  	// stack, but in this case we know there will be no skips in
   222  	// the stack and we have return PCs anyway.
   223  	frames := runtime.CallersFrames([]uintptr{addr})
   224  	frame, more := frames.Next()
   225  	if frame.Function == "runtime.goexit" {
   226  		// Short-circuit if we see runtime.goexit so the loop
   227  		// below doesn't allocate a useless empty location.
   228  		return 0
   229  	}
   230  
   231  	symbolizeResult := lookupTried
   232  	if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
   233  		symbolizeResult |= lookupFailed
   234  	}
   235  
   236  	if frame.PC == 0 {
   237  		// If we failed to resolve the frame, at least make up
   238  		// a reasonable call PC. This mostly happens in tests.
   239  		frame.PC = addr - 1
   240  	}
   241  
   242  	// We can't write out functions while in the middle of the
   243  	// Location message, so record new functions we encounter and
   244  	// write them out after the Location.
   245  	type newFunc struct {
   246  		id         uint64
   247  		name, file string
   248  	}
   249  	newFuncs := make([]newFunc, 0, 8)
   250  
   251  	id = uint64(len(b.locs)) + 1
   252  	b.locs[addr] = int(id)
   253  	start := b.pb.startMessage()
   254  	b.pb.uint64Opt(tagLocation_ID, id)
   255  	b.pb.uint64Opt(tagLocation_Address, uint64(frame.PC))
   256  	for frame.Function != "runtime.goexit" {
   257  		// Write out each line in frame expansion.
   258  		funcID := uint64(b.funcs[frame.Function])
   259  		if funcID == 0 {
   260  			funcID = uint64(len(b.funcs)) + 1
   261  			b.funcs[frame.Function] = int(funcID)
   262  			newFuncs = append(newFuncs, newFunc{funcID, frame.Function, frame.File})
   263  		}
   264  		b.pbLine(tagLocation_Line, funcID, int64(frame.Line))
   265  		if !more {
   266  			break
   267  		}
   268  		frame, more = frames.Next()
   269  	}
   270  	for i := range b.mem {
   271  		if b.mem[i].start <= addr && addr < b.mem[i].end || b.mem[i].fake {
   272  			b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
   273  
   274  			m := b.mem[i]
   275  			m.funcs |= symbolizeResult
   276  			b.mem[i] = m
   277  			break
   278  		}
   279  	}
   280  	b.pb.endMessage(tagProfile_Location, start)
   281  
   282  	// Write out functions we found during frame expansion.
   283  	for _, fn := range newFuncs {
   284  		start := b.pb.startMessage()
   285  		b.pb.uint64Opt(tagFunction_ID, fn.id)
   286  		b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name))
   287  		b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name))
   288  		b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file))
   289  		b.pb.endMessage(tagProfile_Function, start)
   290  	}
   291  
   292  	b.flush()
   293  	return id
   294  }
   295  
   296  // newProfileBuilder returns a new profileBuilder.
   297  // CPU profiling data obtained from the runtime can be added
   298  // by calling b.addCPUData, and then the eventual profile
   299  // can be obtained by calling b.finish.
   300  func newProfileBuilder(w io.Writer) *profileBuilder {
   301  	zw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed)
   302  	b := &profileBuilder{
   303  		w:         w,
   304  		zw:        zw,
   305  		start:     time.Now(),
   306  		strings:   []string{""},
   307  		stringMap: map[string]int{"": 0},
   308  		locs:      map[uintptr]int{},
   309  		funcs:     map[string]int{},
   310  	}
   311  	b.readMapping()
   312  	return b
   313  }
   314  
   315  // addCPUData adds the CPU profiling data to the profile.
   316  // The data must be a whole number of records,
   317  // as delivered by the runtime.
   318  func (b *profileBuilder) addCPUData(data []uint64, tags []unsafe.Pointer) error {
   319  	if !b.havePeriod {
   320  		// first record is period
   321  		if len(data) < 3 {
   322  			return fmt.Errorf("truncated profile")
   323  		}
   324  		if data[0] != 3 || data[2] == 0 {
   325  			return fmt.Errorf("malformed profile")
   326  		}
   327  		// data[2] is sampling rate in Hz. Convert to sampling
   328  		// period in nanoseconds.
   329  		b.period = 1e9 / int64(data[2])
   330  		b.havePeriod = true
   331  		data = data[3:]
   332  	}
   333  
   334  	// Parse CPU samples from the profile.
   335  	// Each sample is 3+n uint64s:
   336  	//	data[0] = 3+n
   337  	//	data[1] = time stamp (ignored)
   338  	//	data[2] = count
   339  	//	data[3:3+n] = stack
   340  	// If the count is 0 and the stack has length 1,
   341  	// that's an overflow record inserted by the runtime
   342  	// to indicate that stack[0] samples were lost.
   343  	// Otherwise the count is usually 1,
   344  	// but in a few special cases like lost non-Go samples
   345  	// there can be larger counts.
   346  	// Because many samples with the same stack arrive,
   347  	// we want to deduplicate immediately, which we do
   348  	// using the b.m profMap.
   349  	for len(data) > 0 {
   350  		if len(data) < 3 || data[0] > uint64(len(data)) {
   351  			return fmt.Errorf("truncated profile")
   352  		}
   353  		if data[0] < 3 || tags != nil && len(tags) < 1 {
   354  			return fmt.Errorf("malformed profile")
   355  		}
   356  		count := data[2]
   357  		stk := data[3:data[0]]
   358  		data = data[data[0]:]
   359  		var tag unsafe.Pointer
   360  		if tags != nil {
   361  			tag = tags[0]
   362  			tags = tags[1:]
   363  		}
   364  
   365  		if count == 0 && len(stk) == 1 {
   366  			// overflow record
   367  			count = uint64(stk[0])
   368  			stk = []uint64{
   369  				uint64(funcPC(lostProfileEvent)),
   370  			}
   371  		}
   372  		b.m.lookup(stk, tag).count += int64(count)
   373  	}
   374  	return nil
   375  }
   376  
   377  // build completes and returns the constructed profile.
   378  func (b *profileBuilder) build() {
   379  	b.end = time.Now()
   380  
   381  	b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano())
   382  	if b.havePeriod { // must be CPU profile
   383  		b.pbValueType(tagProfile_SampleType, "samples", "count")
   384  		b.pbValueType(tagProfile_SampleType, "cpu", "nanoseconds")
   385  		b.pb.int64Opt(tagProfile_DurationNanos, b.end.Sub(b.start).Nanoseconds())
   386  		b.pbValueType(tagProfile_PeriodType, "cpu", "nanoseconds")
   387  		b.pb.int64Opt(tagProfile_Period, b.period)
   388  	}
   389  
   390  	values := []int64{0, 0}
   391  	var locs []uint64
   392  	for e := b.m.all; e != nil; e = e.nextAll {
   393  		values[0] = e.count
   394  		values[1] = e.count * b.period
   395  
   396  		var labels func()
   397  		if e.tag != nil {
   398  			labels = func() {
   399  				for k, v := range *(*labelMap)(e.tag) {
   400  					b.pbLabel(tagSample_Label, k, v, 0)
   401  				}
   402  			}
   403  		}
   404  
   405  		locs = locs[:0]
   406  		for i, addr := range e.stk {
   407  			// Addresses from stack traces point to the
   408  			// next instruction after each call, except
   409  			// for the leaf, which points to where the
   410  			// signal occurred. locForPC expects return
   411  			// PCs, so increment the leaf address to look
   412  			// like a return PC.
   413  			if i == 0 {
   414  				addr++
   415  			}
   416  			l := b.locForPC(addr)
   417  			if l == 0 { // runtime.goexit
   418  				continue
   419  			}
   420  			locs = append(locs, l)
   421  		}
   422  		b.pbSample(values, locs, labels)
   423  	}
   424  
   425  	for i, m := range b.mem {
   426  		hasFunctions := m.funcs == lookupTried // lookupTried but not lookupFailed
   427  		b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(m.start), uint64(m.end), m.offset, m.file, m.buildID, hasFunctions)
   428  	}
   429  
   430  	// TODO: Anything for tagProfile_DropFrames?
   431  	// TODO: Anything for tagProfile_KeepFrames?
   432  
   433  	b.pb.strings(tagProfile_StringTable, b.strings)
   434  	b.zw.Write(b.pb.data)
   435  	b.zw.Close()
   436  }
   437  
   438  // readMapping reads /proc/self/maps and writes mappings to b.pb.
   439  // It saves the address ranges of the mappings in b.mem for use
   440  // when emitting locations.
   441  func (b *profileBuilder) readMapping() {
   442  	data, _ := ioutil.ReadFile("/proc/self/maps")
   443  	parseProcSelfMaps(data, b.addMapping)
   444  	if len(b.mem) == 0 { // pprof expects a map entry, so fake one.
   445  		b.addMappingEntry(0, 0, 0, "", "", true)
   446  		// TODO(hyangah): make addMapping return *memMap or
   447  		// take a memMap struct, and get rid of addMappingEntry
   448  		// that takes a bunch of positional arguments.
   449  	}
   450  }
   451  
   452  func parseProcSelfMaps(data []byte, addMapping func(lo, hi, offset uint64, file, buildID string)) {
   453  	// $ cat /proc/self/maps
   454  	// 00400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat
   455  	// 0060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat
   456  	// 0060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat
   457  	// 014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
   458  	// 7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
   459  	// 7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   460  	// 7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   461  	// 7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   462  	// 7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   463  	// 7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
   464  	// 7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   465  	// 7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
   466  	// 7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
   467  	// 7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   468  	// 7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   469  	// 7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
   470  	// 7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
   471  	// 7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
   472  	// ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
   473  
   474  	var line []byte
   475  	// next removes and returns the next field in the line.
   476  	// It also removes from line any spaces following the field.
   477  	next := func() []byte {
   478  		j := bytes.IndexByte(line, ' ')
   479  		if j < 0 {
   480  			f := line
   481  			line = nil
   482  			return f
   483  		}
   484  		f := line[:j]
   485  		line = line[j+1:]
   486  		for len(line) > 0 && line[0] == ' ' {
   487  			line = line[1:]
   488  		}
   489  		return f
   490  	}
   491  
   492  	for len(data) > 0 {
   493  		i := bytes.IndexByte(data, '\n')
   494  		if i < 0 {
   495  			line, data = data, nil
   496  		} else {
   497  			line, data = data[:i], data[i+1:]
   498  		}
   499  		addr := next()
   500  		i = bytes.IndexByte(addr, '-')
   501  		if i < 0 {
   502  			continue
   503  		}
   504  		lo, err := strconv.ParseUint(string(addr[:i]), 16, 64)
   505  		if err != nil {
   506  			continue
   507  		}
   508  		hi, err := strconv.ParseUint(string(addr[i+1:]), 16, 64)
   509  		if err != nil {
   510  			continue
   511  		}
   512  		perm := next()
   513  		if len(perm) < 4 || perm[2] != 'x' {
   514  			// Only interested in executable mappings.
   515  			continue
   516  		}
   517  		offset, err := strconv.ParseUint(string(next()), 16, 64)
   518  		if err != nil {
   519  			continue
   520  		}
   521  		next()          // dev
   522  		inode := next() // inode
   523  		if line == nil {
   524  			continue
   525  		}
   526  		file := string(line)
   527  		if len(inode) == 1 && inode[0] == '0' && file == "" {
   528  			// Huge-page text mappings list the initial fragment of
   529  			// mapped but unpopulated memory as being inode 0.
   530  			// Don't report that part.
   531  			// But [vdso] and [vsyscall] are inode 0, so let non-empty file names through.
   532  			continue
   533  		}
   534  
   535  		// TODO: pprof's remapMappingIDs makes two adjustments:
   536  		// 1. If there is an /anon_hugepage mapping first and it is
   537  		// consecutive to a next mapping, drop the /anon_hugepage.
   538  		// 2. If start-offset = 0x400000, change start to 0x400000 and offset to 0.
   539  		// There's no indication why either of these is needed.
   540  		// Let's try not doing these and see what breaks.
   541  		// If we do need them, they would go here, before we
   542  		// enter the mappings into b.mem in the first place.
   543  
   544  		buildID, _ := elfBuildID(file)
   545  		addMapping(lo, hi, offset, file, buildID)
   546  	}
   547  }
   548  
   549  func (b *profileBuilder) addMapping(lo, hi, offset uint64, file, buildID string) {
   550  	b.addMappingEntry(lo, hi, offset, file, buildID, false)
   551  }
   552  
   553  func (b *profileBuilder) addMappingEntry(lo, hi, offset uint64, file, buildID string, fake bool) {
   554  	b.mem = append(b.mem, memMap{
   555  		start:   uintptr(lo),
   556  		end:     uintptr(hi),
   557  		offset:  offset,
   558  		file:    file,
   559  		buildID: buildID,
   560  		fake:    fake,
   561  	})
   562  }
   563  

View as plain text