Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions bitset.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ func (p *ParseOption) Set(n ParseOption) {
}

func (p ParseOption) IsSet(n ParseOption) bool {
return p & n != 0
return p&n != 0
}

func (p *LoadSubsetOption) Set(n LoadSubsetOption) {
*p = *p | n
}

func (p LoadSubsetOption) IsSet(n LoadSubsetOption) bool {
return p & n != 0
return p&n != 0
}

188 changes: 188 additions & 0 deletions cmd/helium-flamegraph/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package main

import (
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"runtime/pprof"

"github.com/lestrrat-go/helium"
)

const usage = `helium-flamegraph - Generate flamegraphs for helium XML operations

Usage:
helium-flamegraph [options] <xml-file>

Options:
-iterations int Number of parsing iterations (default: 2000)
-port int HTTP server port (default: 8080)
-profile string Profile type: cpu, mem (default: cpu)
-help Show this help message

This command will:
1. Generate a profile by running XML operations
2. Start pprof web server to view the flamegraph

Examples:
helium-flamegraph sample.xml # CPU flamegraph on port 8080
helium-flamegraph -profile mem sample.xml # Memory flamegraph
helium-flamegraph -port 9090 sample.xml # Use different port
`

func main() {
var (
iterations = flag.Int("iterations", 2000, "Number of parsing iterations")
port = flag.Int("port", 8080, "HTTP server port")
profile = flag.String("profile", "cpu", "Profile type: cpu, mem")
help = flag.Bool("help", false, "Show help message")
)

flag.Parse()

if *help {
fmt.Print(usage)
os.Exit(0)
}

if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "Error: XML file argument required\n\n")
fmt.Print(usage)
os.Exit(1)
}

xmlFile := flag.Arg(0)

if _, err := os.Stat(xmlFile); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: XML file does not exist: %s\n", xmlFile)
os.Exit(1)
}

if *profile != "cpu" && *profile != "mem" {
fmt.Fprintf(os.Stderr, "Error: profile must be 'cpu' or 'mem'\n")
os.Exit(1)
}

if err := generateAndStartServer(xmlFile, *iterations, *port, *profile); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func generateAndStartServer(xmlFile string, iterations, port int, profileType string) error {
fmt.Printf("🔥 Helium Flamegraph Generator\n")
fmt.Printf("XML file: %s\n", xmlFile)
fmt.Printf("Profile type: %s\n", profileType)
fmt.Printf("Iterations: %d\n", iterations)
fmt.Printf("Server port: %d\n\n", port)

// Read XML file
xmlData, err := os.ReadFile(xmlFile)
if err != nil {
return fmt.Errorf("failed to read XML file: %w", err)
}

profileFile := fmt.Sprintf("helium_%s.prof", profileType)

// Generate profile
fmt.Printf("📊 Generating %s profile...\n", profileType)
if err := generateProfile(xmlData, iterations, profileType, profileFile); err != nil {
return fmt.Errorf("failed to generate profile: %w", err)
}

fmt.Printf("✅ Profile generated: %s\n\n", profileFile)

// Start pprof web server
return startPprofServer(profileFile, port)
}

func generateProfile(xmlData []byte, iterations int, profileType, profileFile string) error {
// Disable tracing for performance
helium.SetTracingEnabled(false)
ctx := context.Background()
parser := helium.NewParser()

switch profileType {
case "cpu":
return generateCPUProfile(ctx, parser, xmlData, iterations, profileFile)
case "mem":
return generateMemProfile(ctx, parser, xmlData, iterations, profileFile)
default:
return fmt.Errorf("unsupported profile type: %s", profileType)
}
}

func generateCPUProfile(ctx context.Context, parser *helium.Parser, xmlData []byte, iterations int, profileFile string) error {
f, err := os.Create(profileFile)
if err != nil {
return err
}
defer f.Close()

if err := pprof.StartCPUProfile(f); err != nil {
return err
}
defer pprof.StopCPUProfile()

// Run workload
for i := range iterations {
doc, err := parser.Parse(ctx, xmlData)
if err != nil {
return fmt.Errorf("parse failed at iteration %d: %w", i, err)
}

// Also test serialization to get complete picture
if err := doc.XML(ctx, io.Discard); err != nil {
return fmt.Errorf("serialization failed at iteration %d: %w", i, err)
}
}

return nil
}

func generateMemProfile(ctx context.Context, parser *helium.Parser, xmlData []byte, iterations int, profileFile string) error {
// Create many documents to trigger allocations
var docs []*helium.Document
for range iterations {
doc, err := parser.Parse(ctx, xmlData)
if err != nil {
return fmt.Errorf("parse failed: %w", err)
}
docs = append(docs, doc)
}

// Write memory profile
f, err := os.Create(profileFile)
if err != nil {
return err
}
defer f.Close()

if err := pprof.WriteHeapProfile(f); err != nil {
return err
}

// Prevent optimization of docs slice
_ = len(docs)
return nil
}

func startPprofServer(profileFile string, port int) error {
fmt.Printf("🌐 Starting pprof server on port %d...\n", port)
fmt.Printf("📍 Open browser to: http://localhost:%d/ui/\n\n", port)

// Start pprof server
cmd := exec.Command("go", "tool", "pprof", "-http", fmt.Sprintf(":%d", port), profileFile)

fmt.Printf("📋 Instructions:\n")
fmt.Printf(" • The pprof web interface will start shortly\n")
fmt.Printf(" • Open http://localhost:%d/ui/ in your browser\n", port)
fmt.Printf(" • Click 'Flame Graph' for flamegraph view\n")
fmt.Printf(" • Press Ctrl+C to stop the server when done\n\n")

// Run server (blocks until interrupted)
return cmd.Run()
}
5 changes: 3 additions & 2 deletions cmd/helium-lint/helium-lint.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -76,14 +77,14 @@ func _main() int {
fmt.Fprintf(os.Stderr, "%s\n", err)
return 1
}
doc, err := p.Parse(buf)
doc, err := p.Parse(context.Background(), buf)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return 1
}

d := helium.Dumper{}
if err := d.DumpDoc(os.Stdout, doc); err != nil {
if err := d.DumpDoc(context.Background(), os.Stdout, doc); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return 1
}
Expand Down
59 changes: 7 additions & 52 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package helium

import (
"bytes"
"context"
"errors"
"io"
"strings"

"github.com/lestrrat-go/pdebug"
"github.com/lestrrat-go/helium/node"
)

func CreateDocument() *Document {
return NewDocument("1.0", "", StandaloneImplicitNo)
func CreateDocument() *node.Document {
return node.NewDocument()
}

func NewDocument(version, encoding string, standalone DocumentStandaloneType) *Document {
Expand All @@ -27,14 +28,14 @@ func NewDocument(version, encoding string, standalone DocumentStandaloneType) *D

func (d Document) XMLString() (string, error) {
out := bytes.Buffer{}
if err := d.XML(&out); err != nil {
if err := d.XML(context.Background(), &out); err != nil {
return "", err
}
return out.String(), nil
}

func (d *Document) XML(out io.Writer) error {
return (&Dumper{}).DumpDoc(out, d)
func (d *Document) XML(ctx context.Context, out io.Writer) error {
return (&Dumper{}).DumpDoc(ctx, out, d)
}

func (d *Document) AddChild(cur Node) error {
Expand Down Expand Up @@ -112,10 +113,6 @@ func (d *Document) SetDocumentElement(root Node) error {
}

func (d *Document) CreateReference(name string) (*EntityRef, error) {
if pdebug.Enabled {
g := pdebug.IPrintf("START document.CreateReference '%s'", name)
defer g.IRelease("END document.CreateReference")
}
n, err := d.CreateCharRef(name)
if err != nil {
return nil, err
Expand All @@ -135,12 +132,6 @@ func (d *Document) CreateReference(name string) (*EntityRef, error) {
}

func (d *Document) CreateAttribute(name, value string, ns *Namespace) (attr *Attribute, err error) {
if pdebug.Enabled {
g := pdebug.IPrintf("START document.CreateAttribute '%s' (%s)", name, value)
defer func() {
g.IRelease("END document.CreateAttribute (attr.Value = '%s')", attr.Value())
}()
}
var n Node
attr = newAttribute(name, ns)
if value != "" {
Expand Down Expand Up @@ -249,28 +240,12 @@ func (d *Document) CreateElementContent(name string, etype ElementContentType) (
}

func (d *Document) GetEntity(name string) (ent *Entity, found bool) {
if pdebug.Enabled {
g := pdebug.IPrintf("START document.GetEntity '%s'", name)
defer func() {
if found {
g.IRelease("END document.GetEntity found = %t '%#x', (%p)", found, ent.Content(), ent)
} else {
g.IRelease("END document.GetEntity found = false")
}
}()
}
if ints := d.intSubset; ints != nil {
if pdebug.Enabled {
pdebug.Printf("Looking into internal subset...")
}
ent, found = ints.LookupEntity(name)
return
}

if exts := d.extSubset; exts != nil {
if pdebug.Enabled {
pdebug.Printf("Looking into external subset...")
}
ent, found = exts.LookupEntity(name)
return
}
Expand Down Expand Up @@ -324,18 +299,6 @@ func (d *Document) IsMixedElement(name string) (bool, error) {
* Returns a pointer to the first child
*/
func (d *Document) stringToNodeList(value string) (ret Node, err error) {
if pdebug.Enabled {
g := pdebug.IPrintf("START document.stringToNodeList '%s'", value)
defer func() {
var content []byte
if ret == nil {
content = []byte("(nil)")
} else {
content = ret.Content()
}
g.IRelease("END document.stringToNodeList '%s'", content)
}()
}
rdr := strings.NewReader(value)
buf := bytes.Buffer{}
var last Node
Expand Down Expand Up @@ -422,9 +385,6 @@ func (d *Document) stringToNodeList(value string) (ret Node, err error) {
} else {
// flush the buffer so far
if buf.Len() > 0 {
if pdebug.Enabled {
pdebug.Printf("Flushing content so far... '%s'", buf.Bytes())
}
var node Node
node, err = d.CreateText(buf.Bytes())
if err != nil {
Expand Down Expand Up @@ -509,11 +469,6 @@ func (d *Document) stringToNodeList(value string) (ret Node, err error) {
}

func (d *Document) CreateCharRef(name string) (*EntityRef, error) {
if pdebug.Enabled {
g := pdebug.IPrintf("START document.CreateCharRef '%s'", name)
defer g.IRelease("END document.CreateCharRef")
}

if name == "" {
return nil, errors.New("empty name")
}
Expand Down
7 changes: 0 additions & 7 deletions dtd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"errors"
"fmt"
"strings"

"github.com/lestrrat-go/pdebug"
)

func newDTD() *DTD {
Expand Down Expand Up @@ -46,11 +44,6 @@ func (dtd *DTD) AddEntity(name string, typ EntityType, publicID, systemID, conte
}

func (dtd *DTD) AddElementDecl(name string, typ ElementTypeVal, content *ElementContent) (*ElementDecl, error) {
if pdebug.Enabled {
g := pdebug.IPrintf("START dtd.AddElementDecl '%s'", name)
defer g.IRelease("END dtd.AddElementDecl")
}

switch typ {
case EmptyElementType, AnyElementType:
if content != nil {
Expand Down
Loading