root/trunk/pdns/pdns/speedtest.cc @ 1670

Revision 1670, 18.9 KB (checked in by ahu, 3 years ago)

make sure speedtest works on solaris too - it needs the virtual alarm to be re-armed after it went off

Line 
1#include "dnsparser.hh"
2#include "sstuff.hh"
3#include "misc.hh"
4#include "dnswriter.hh"
5#include "dnsrecords.hh"
6#include <boost/format.hpp>
7#ifndef RECURSOR
8#include "statbag.hh"
9StatBag S;
10#endif
11
12uint64_t g_totalRuns;
13
14volatile bool g_stop;
15
16void alarmHandler(int)
17{
18  g_stop=true;
19}
20
21template<typename C> void doRun(const C& cmd, int mseconds=100)
22{
23  struct itimerval it;
24  it.it_value.tv_sec=mseconds/1000;
25  it.it_value.tv_usec = 1000* (mseconds%1000);
26  it.it_interval.tv_sec=0;
27  it.it_interval.tv_usec=0;
28
29  signal(SIGVTALRM, alarmHandler);
30  setitimer(ITIMER_VIRTUAL, &it, 0);
31 
32  unsigned int runs=0;
33  g_stop=false;
34  DTime dt;
35  dt.set();
36  while(runs++, !g_stop) {
37    cmd();
38  }
39  double delta=dt.udiff()/1000000.0;
40  boost::format fmt("'%s' %.02f seconds: %.1f runs/s, %.02f usec/run");
41
42  cerr<< (fmt % cmd.getName() % delta % (runs/delta) % (delta* 1000000.0/runs)) << endl;
43  g_totalRuns += runs;
44}
45
46struct ARecordTest
47{
48  explicit ARecordTest(int records) : d_records(records) {}
49
50  string getName() const
51  {
52    return (boost::format("%d a records") % d_records).str();
53  }
54
55  void operator()() const
56  {
57    vector<uint8_t> packet;
58    DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::A);
59    for(int records = 0; records < d_records; records++) {
60      pw.startRecord("outpost.ds9a.nl", QType::A);
61      ARecordContent arc("1.2.3.4");
62      arc.toPacket(pw);
63    }
64    pw.commit();
65  }
66  int d_records;
67};
68
69
70struct MakeStringFromCharStarTest
71{
72  MakeStringFromCharStarTest() : d_size(0){}
73  string getName() const
74  {
75    return (boost::format("make a std::string")).str();
76  }
77
78  void operator()() const
79  {
80    string name("outpost.ds9a.nl");
81    d_size += name.length();
82   
83  }
84  mutable int d_size;
85};
86
87struct MakeARecordTest
88{
89  string getName() const
90  {
91    return (boost::format("make a-record")).str();
92  }
93
94  void operator()() const
95  {
96      static string src("1.2.3.4");
97      ARecordContent arc(src);
98      //ARecordContent arc(0x01020304);
99
100  }
101};
102
103struct MakeARecordTestMM
104{
105  string getName() const
106  {
107    return (boost::format("make a-record (mm)")).str();
108  }
109
110  void operator()() const
111  {
112      DNSRecordContent*drc = DNSRecordContent::mastermake(QType::A, 1, 
113                                                          "1.2.3.4");
114      delete drc;
115  }
116};
117
118
119struct A2RecordTest
120{
121  explicit A2RecordTest(int records) : d_records(records) {}
122
123  string getName() const
124  {
125    return (boost::format("%d a records") % d_records).str();
126  }
127
128  void operator()() const
129  {
130    vector<uint8_t> packet;
131    DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::A);
132    ARecordContent arc("1.2.3.4");
133    string name("outpost.ds9a.nl");
134    for(int records = 0; records < d_records; records++) {
135      pw.startRecord(name, QType::A);
136
137      arc.toPacket(pw);
138    }
139    pw.commit();
140  }
141  int d_records;
142};
143
144
145struct TXTRecordTest
146{
147  explicit TXTRecordTest(int records) : d_records(records) {}
148
149  string getName() const
150  {
151    return (boost::format("%d TXT records") % d_records).str();
152  }
153
154  void operator()() const
155  {
156    vector<uint8_t> packet;
157    DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::TXT);
158    for(int records = 0; records < d_records; records++) {
159      pw.startRecord("outpost.ds9a.nl", QType::TXT);
160      TXTRecordContent arc("\"een leuk verhaaltje in een TXT\"");
161      arc.toPacket(pw);
162    }
163    pw.commit();
164  }
165  int d_records;
166};
167
168
169struct GenericRecordTest
170{
171  explicit GenericRecordTest(int records, uint16_t type, const std::string& content) 
172    : d_records(records), d_type(type), d_content(content) {}
173
174  string getName() const
175  {
176    return (boost::format("%d %s records") % d_records % 
177            DNSRecordContent::NumberToType(d_type)).str();
178  }
179
180  void operator()() const
181  {
182    vector<uint8_t> packet;
183    DNSPacketWriter pw(packet, "outpost.ds9a.nl", d_type);
184    for(int records = 0; records < d_records; records++) {
185      pw.startRecord("outpost.ds9a.nl", d_type);
186      DNSRecordContent*drc = DNSRecordContent::mastermake(d_type, 1, 
187                                                          d_content);
188      drc->toPacket(pw);
189      delete drc;
190    }
191    pw.commit();
192  }
193  int d_records;
194  uint16_t d_type;
195  string d_content;
196};
197
198
199struct AAAARecordTest
200{
201  explicit AAAARecordTest(int records) : d_records(records) {}
202
203  string getName() const
204  {
205    return (boost::format("%d aaaa records (mm)") % d_records).str();
206  }
207
208  void operator()() const
209  {
210    vector<uint8_t> packet;
211    DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::AAAA);
212    for(int records = 0; records < d_records; records++) {
213      pw.startRecord("outpost.ds9a.nl", QType::AAAA);
214      DNSRecordContent*drc = DNSRecordContent::mastermake(QType::AAAA, 1, "fe80::21d:92ff:fe6d:8441");
215      drc->toPacket(pw);
216      delete drc;
217    }
218    pw.commit();
219  }
220  int d_records;
221};
222
223struct SOARecordTest
224{
225  explicit SOARecordTest(int records) : d_records(records) {}
226
227  string getName() const
228  {
229    return (boost::format("%d soa records (mm)") % d_records).str();
230  }
231
232  void operator()() const
233  {
234    vector<uint8_t> packet;
235    DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::SOA);
236
237    for(int records = 0; records < d_records; records++) {
238      pw.startRecord("outpost.ds9a.nl", QType::SOA);
239      DNSRecordContent*drc = DNSRecordContent::mastermake(QType::SOA, 1, "a0.org.afilias-nst.info. noc.afilias-nst.info. 2008758137 1800 900 604800 86400");
240      drc->toPacket(pw);
241      delete drc;
242    }
243    pw.commit();
244  }
245  int d_records;
246};
247
248vector<uint8_t> makeEmptyQuery()
249{
250  vector<uint8_t> packet;
251  DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::SOA);
252  return  packet;
253}
254
255
256vector<uint8_t> makeRootReferral()
257{
258  vector<uint8_t> packet;
259  DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::SOA);
260
261  // nobody reads what we output, but it appears to be the magic that shuts some nameservers up
262  static const char*ips[]={"198.41.0.4", "192.228.79.201", "192.33.4.12", "128.8.10.90", "192.203.230.10", "192.5.5.241", "192.112.36.4", "128.63.2.53", 
263                     "192.36.148.17","192.58.128.30", "193.0.14.129", "198.32.64.12", "202.12.27.33"};
264  static char templ[40];
265  strncpy(templ,"a.root-servers.net", sizeof(templ) - 1);
266 
267 
268  for(char c='a';c<='m';++c) {
269    *templ=c;
270    pw.startRecord(".", QType::NS, 3600, 1, DNSPacketWriter::AUTHORITY);
271    DNSRecordContent* drc = DNSRecordContent::mastermake(QType::NS, 1, templ);
272    drc->toPacket(pw);
273    delete drc;
274  }
275
276  for(char c='a';c<='m';++c) {
277    *templ=c;
278    pw.startRecord(".", QType::A, 3600, 1, DNSPacketWriter::ADDITIONAL);
279    DNSRecordContent* drc = DNSRecordContent::mastermake(QType::A, 1, ips[c-'a']);
280    drc->toPacket(pw);
281    delete drc;
282  }
283  pw.commit();
284  return  packet;
285
286}
287
288vector<uint8_t> makeTypicalReferral()
289{
290  vector<uint8_t> packet;
291  DNSPacketWriter pw(packet, "outpost.ds9a.nl", QType::A);
292
293  pw.startRecord("ds9a.nl", QType::NS, 3600, 1, DNSPacketWriter::AUTHORITY);
294  DNSRecordContent* drc = DNSRecordContent::mastermake(QType::NS, 1, "ns1.ds9a.nl");
295  drc->toPacket(pw);
296  delete drc;
297
298  pw.startRecord("ds9a.nl", QType::NS, 3600, 1, DNSPacketWriter::AUTHORITY);
299  drc = DNSRecordContent::mastermake(QType::NS, 1, "ns2.ds9a.nl");
300  drc->toPacket(pw);
301  delete drc;
302
303
304  pw.startRecord("ns1.ds9a.nl", QType::A, 3600, 1, DNSPacketWriter::ADDITIONAL);
305  drc = DNSRecordContent::mastermake(QType::A, 1, "1.2.3.4");
306  drc->toPacket(pw);
307  delete drc;
308
309  pw.startRecord("ns2.ds9a.nl", QType::A, 3600, 1, DNSPacketWriter::ADDITIONAL);
310  drc = DNSRecordContent::mastermake(QType::A, 1, "4.3.2.1");
311  drc->toPacket(pw);
312  delete drc;
313
314  pw.commit();
315  return  packet;
316}
317
318
319
320struct RootRefTest
321{
322  string getName() const
323  {
324    return "write rootreferral";
325  }
326
327  void operator()() const
328  {
329    vector<uint8_t> packet=makeRootReferral();
330  }
331
332};
333
334struct StackMallocTest
335{
336  string getName() const
337  {
338    return "stack allocation";
339  }
340
341  void operator()() const
342  {
343    char *buffer= new char[200000];
344    delete buffer;
345  }
346
347};
348
349
350struct EmptyQueryTest
351{
352  string getName() const
353  {
354    return "write empty query";
355  }
356
357  void operator()() const
358  {
359    vector<uint8_t> packet=makeEmptyQuery();
360  }
361
362};
363
364struct TypicalRefTest
365{
366  string getName() const
367  {
368    return "write typical referral";
369  }
370
371  void operator()() const
372  {
373    vector<uint8_t> packet=makeTypicalReferral();
374  }
375
376};
377
378struct TCacheComp
379{
380  bool operator()(const pair<string, QType>& a, const pair<string, QType>& b) const
381  {
382    int cmp=strcasecmp(a.first.c_str(), b.first.c_str());
383    if(cmp < 0)
384      return true;
385    if(cmp > 0)
386      return false;
387
388    return a.second < b.second;
389  }
390};
391
392struct NegCacheEntry
393{
394  string d_name;
395  QType d_qtype;
396  string d_qname;
397  uint32_t d_ttd;
398};
399
400struct timeval d_now;
401
402static bool magicAddrMatch(const QType& query, const QType& answer)
403{
404  if(query.getCode() != QType::ADDR)
405    return false;
406  return answer.getCode() == QType::A || answer.getCode() == QType::AAAA;
407}
408
409
410bool moreSpecificThan(const string& a, const string &b)
411{
412  static string dot(".");
413  int counta=(a!=dot), countb=(b!=dot);
414 
415  for(string::size_type n=0;n<a.size();++n)
416    if(a[n]=='.')
417      counta++;
418  for(string::size_type n=0;n<b.size();++n)
419    if(b[n]=='.')
420      countb++;
421  return counta>countb;
422}
423
424
425struct ParsePacketTest
426{
427  explicit ParsePacketTest(const vector<uint8_t>& packet, const std::string& name) 
428    : d_packet(packet), d_name(name)
429  {}
430
431  string getName() const
432  {
433    return "parse '"+d_name+"'";
434  }
435
436  void operator()() const
437  {
438    MOADNSParser mdp((const char*)&*d_packet.begin(), d_packet.size());
439    typedef map<pair<string, QType>, set<DNSResourceRecord>, TCacheComp > tcache_t;
440    tcache_t tcache;
441   
442    struct {
443            vector<DNSResourceRecord> d_result;
444            bool d_aabit;
445            int d_rcode;
446    } lwr;
447    DNSResourceRecord rr;
448    for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i!=mdp.d_answers.end(); ++i) {         
449      DNSResourceRecord rr;
450      rr.qtype=i->first.d_type;
451      rr.qname=i->first.d_label;
452   
453      rr.ttl=i->first.d_ttl;
454      rr.content=i->first.d_content->getZoneRepresentation();  // this should be the serialised form
455      rr.d_place=(DNSResourceRecord::Place) i->first.d_place;
456      lwr.d_result.push_back(rr);
457    }
458
459   
460   
461
462      // reap all answers from this packet that are acceptable
463      for(vector<DNSResourceRecord>::iterator i=lwr.d_result.begin();i != lwr.d_result.end();++i) {
464        if(i->qtype.getCode() == QType::OPT) {
465          // <<prefix<<qname<<": skipping OPT answer '"<<i->qname<<"' from '"<<auth<<"' nameservers" <<endl;
466          continue;
467        }
468        // LOG<<prefix<<qname<<": accept answer '"<<i->qname<<"|"<<i->qtype.getName()<<"|"<<i->content<<"' from '"<<auth<<"' nameservers? ";
469        if(i->qtype.getCode()==QType::ANY) {
470          // LOG<<"NO! - we don't accept 'ANY' data"<<endl;
471          continue;
472        }
473        string auth(".");
474        if(dottedEndsOn(i->qname, auth)) {
475          if(lwr.d_aabit && lwr.d_rcode==RCode::NoError && i->d_place==DNSResourceRecord::ANSWER && 0) {
476            // LOG<<"NO! Is from delegation-only zone"<<endl;
477            // s_nodelegated++;
478            return; // RCode::NXDomain;
479          }
480          else {
481            // LOG<<"YES!"<<endl;
482
483          //  i->ttl=min(s_maxcachettl, i->ttl);
484           
485            DNSResourceRecord rr=*i;
486            rr.d_place=DNSResourceRecord::ANSWER;
487
488            // rr.ttl += d_now.tv_sec;
489
490            if(rr.qtype.getCode() == QType::NS) // people fiddle with the case
491              rr.content=toLower(rr.content); // this must stay! (the cache can't be case-insensitive on the RHS of records)
492            tcache[make_pair(i->qname,i->qtype)].insert(rr);
493          }
494        }         
495        else
496          ; // LOG<<"NO!"<<endl;
497      }
498   
499      // supplant
500      for(tcache_t::iterator i=tcache.begin();i!=tcache.end();++i) {
501        if(i->second.size() > 1) {  // need to group the ttl to be the minimum of the RRSET (RFC 2181, 5.2)
502          uint32_t lowestTTL=numeric_limits<uint32_t>::max();
503          for(tcache_t::value_type::second_type::iterator j=i->second.begin(); j != i->second.end(); ++j)
504            lowestTTL=min(lowestTTL, j->ttl);
505         
506          for(tcache_t::value_type::second_type::iterator j=i->second.begin(); j != i->second.end(); ++j)
507            ((tcache_t::value_type::second_type::value_type*)&(*j))->ttl=lowestTTL;
508        }
509
510        // RC.replace(d_now.tv_sec, i->first.first, i->first.second, i->second, lwr.d_aabit);
511      }
512      set<string, CIStringCompare> nsset; 
513      // LOG<<prefix<<qname<<": determining status after receiving this packet"<<endl;
514
515      bool done=false, realreferral=false, negindic=false;
516      string newauth, soaname, newtarget;
517      string qname(".");
518      vector<DNSResourceRecord> ret;
519      QType qtype(QType::A);
520      string auth(".");
521 
522      for(vector<DNSResourceRecord>::const_iterator i=lwr.d_result.begin();i!=lwr.d_result.end();++i) {
523        if(i->d_place==DNSResourceRecord::AUTHORITY && dottedEndsOn(qname,i->qname) && i->qtype.getCode()==QType::SOA && 
524           lwr.d_rcode==RCode::NXDomain) {
525          // LOG<<prefix<<qname<<": got negative caching indication for RECORD '"<<qname+"'"<<endl;
526          ret.push_back(*i);
527
528          NegCacheEntry ne;
529
530          ne.d_qname=i->qname;
531          ne.d_ttd=d_now.tv_sec + min(i->ttl, 3600U); // controversial
532          ne.d_name=qname;
533          ne.d_qtype=QType(0); // this encodes 'whole record'
534         
535          {
536            // Lock l(&s_negcachelock);
537            // replacing_insert(s_negcache, ne);
538          }
539          negindic=true;
540        }
541        else if(i->d_place==DNSResourceRecord::ANSWER && pdns_iequals(i->qname, qname) && i->qtype.getCode()==QType::CNAME && (!(qtype==QType(QType::CNAME)))) {
542          ret.push_back(*i);
543          newtarget=i->content;
544        }
545        // for ANY answers we *must* have an authoritive answer
546        else if(i->d_place==DNSResourceRecord::ANSWER && pdns_iequals(i->qname, qname) && 
547                (
548                 i->qtype==qtype || (lwr.d_aabit && (qtype==QType(QType::ANY) || magicAddrMatch(qtype, i->qtype) ) )
549                ) 
550               )   
551          {
552         
553          // LOG<<prefix<<qname<<": answer is in: resolved to '"<< i->content<<"|"<<i->qtype.getName()<<"'"<<endl;
554
555          done=true;
556          ret.push_back(*i);
557        }
558        else if(i->d_place==DNSResourceRecord::AUTHORITY && dottedEndsOn(qname,i->qname) && i->qtype.getCode()==QType::NS) { 
559          if(moreSpecificThan(i->qname,auth)) {
560            newauth=i->qname;
561            // LOG<<prefix<<qname<<": got NS record '"<<i->qname<<"' -> '"<<i->content<<"'"<<endl;
562            realreferral=true;
563          }
564          else 
565            ;// // LOG<<prefix<<qname<<": got upwards/level NS record '"<<i->qname<<"' -> '"<<i->content<<"', had '"<<auth<<"'"<<endl;
566          nsset.insert(i->content);
567        }
568        else if(!done && i->d_place==DNSResourceRecord::AUTHORITY && dottedEndsOn(qname,i->qname) && i->qtype.getCode()==QType::SOA && 
569           lwr.d_rcode==RCode::NoError) {
570          // LOG<<prefix<<qname<<": got negative caching indication for '"<< (qname+"|"+i->qtype.getName()+"'") <<endl;
571          ret.push_back(*i);
572         
573          NegCacheEntry ne;
574          ne.d_qname=i->qname;
575          ne.d_ttd=d_now.tv_sec + i->ttl;
576          ne.d_name=qname;
577          ne.d_qtype=qtype;
578          if(qtype.getCode()) {  // prevents us from blacking out a whole domain
579           // Lock l(&s_negcachelock);
580            // replacing_insert(s_negcache, ne);
581          }
582          negindic=true;
583        }
584      }
585
586  }
587  const vector<uint8_t>& d_packet;
588  std::string d_name;
589};
590
591struct ParsePacketBareTest
592{
593  explicit ParsePacketBareTest(const vector<uint8_t>& packet, const std::string& name) 
594    : d_packet(packet), d_name(name)
595  {}
596
597  string getName() const
598  {
599    return "parse '"+d_name+"' bare";
600  }
601
602  void operator()() const
603  {
604    MOADNSParser mdp((const char*)&*d_packet.begin(), d_packet.size());
605  }
606  const vector<uint8_t>& d_packet;
607  std::string d_name;
608};
609
610
611struct SimpleCompressTest
612{
613  explicit SimpleCompressTest(const std::string& name) 
614    : d_name(name)
615  {}
616
617  string getName() const
618  {
619    return "compress '"+d_name+"'";
620  }
621
622  void operator()() const
623  {
624    simpleCompress(d_name);
625  }
626  std::string d_name;
627};
628
629struct VectorExpandTest
630{
631  string getName() const
632  {
633    return "vector expand";
634  }
635
636  void operator()() const
637  {
638    vector<uint8_t> d_record;
639    d_record.resize(12);
640
641    string out="\x03www\x04ds9a\x02nl";
642    string::size_type len = d_record.size();
643    d_record.resize(len + out.length());
644    memcpy(&d_record[len], out.c_str(), out.length());
645  }
646
647};
648
649
650
651struct IEqualsTest
652{
653  string getName() const
654  {
655    return "iequals test";
656  }
657
658  void operator()() const
659  {
660      static string a("www.ds9a.nl"), b("www.lwn.net");
661      bool ret = boost::iequals(a, b);
662  }
663
664};
665
666struct MyIEqualsTest
667{
668  string getName() const
669  {
670    return "pdns_iequals test";
671  }
672
673  void operator()() const
674  {
675      static string a("www.ds9a.nl"), b("www.lwn.net");
676      bool ret = pdns_iequals(a, b);
677  }
678
679};
680
681
682struct StrcasecmpTest
683{
684  string getName() const
685  {
686    return "strcasecmp test";
687  }
688
689  void operator()() const
690  {
691      static string a("www.ds9a.nl"), b("www.lwn.net");
692      bool ret = strcasecmp(a.c_str(), b.c_str());
693  }
694};
695
696
697struct NOPTest
698{
699  string getName() const
700  {
701    return "null test";
702  }
703
704  void operator()() const
705  {
706  }
707
708};
709
710
711
712int main(int argc, char** argv)
713try
714{
715  reportAllTypes();
716  doRun(NOPTest());
717
718  doRun(IEqualsTest());
719  doRun(MyIEqualsTest());
720  doRun(StrcasecmpTest());
721
722  doRun(StackMallocTest());
723
724  vector<uint8_t> packet = makeRootReferral();
725  doRun(ParsePacketBareTest(packet, "root-referral"));
726  doRun(ParsePacketTest(packet, "root-referral"));
727
728  doRun(RootRefTest());
729
730  doRun(EmptyQueryTest());
731  doRun(TypicalRefTest());
732
733
734  packet = makeEmptyQuery();
735  doRun(ParsePacketTest(packet, "empty-query"));
736
737  packet = makeTypicalReferral();
738  cerr<<"typical referral size: "<<packet.size()<<endl;
739  doRun(ParsePacketBareTest(packet, "typical-referral"));
740
741  doRun(ParsePacketTest(packet, "typical-referral"));
742
743  doRun(SimpleCompressTest("www.france.ds9a.nl"));
744
745 
746  doRun(VectorExpandTest());
747
748  doRun(ARecordTest(1));
749  doRun(ARecordTest(2));
750  doRun(ARecordTest(4));
751  doRun(ARecordTest(64));
752
753  doRun(A2RecordTest(1));
754  doRun(A2RecordTest(2));
755  doRun(A2RecordTest(4));
756  doRun(A2RecordTest(64));
757
758  doRun(MakeStringFromCharStarTest());
759  doRun(MakeARecordTest());
760  doRun(MakeARecordTestMM());
761
762  doRun(AAAARecordTest(1));
763  doRun(AAAARecordTest(2));
764  doRun(AAAARecordTest(4));
765  doRun(AAAARecordTest(64));
766
767  doRun(TXTRecordTest(1));
768  doRun(TXTRecordTest(2));
769  doRun(TXTRecordTest(4));
770  doRun(TXTRecordTest(64));
771
772  doRun(GenericRecordTest(1, QType::NS, "powerdnssec1.ds9a.nl"));
773  doRun(GenericRecordTest(2, QType::NS, "powerdnssec1.ds9a.nl"));
774  doRun(GenericRecordTest(4, QType::NS, "powerdnssec1.ds9a.nl"));
775  doRun(GenericRecordTest(64, QType::NS, "powerdnssec1.ds9a.nl"));
776
777
778
779  doRun(SOARecordTest(1));
780  doRun(SOARecordTest(2));
781  doRun(SOARecordTest(4));
782  doRun(SOARecordTest(64));
783
784  cerr<<"Total runs: " << g_totalRuns<<endl;
785
786}
787catch(std::exception &e)
788{
789  cerr<<"Fatal: "<<e.what()<<endl;
790}
791
Note: See TracBrowser for help on using the browser.