Optimize ninja_writer

Prebuild a string of all spaces to avoid calling strings.Repeat for
every indented line.  Replace appendWithWrap function with a class
that handles counting bytes on a line as they are written instead of
buffering lines, and avoids extra string concatenations by providing
a function that implicitly adds a space.  Saves 10% wall time on one
workload.

Change-Id: I571d70366ee5db21c40c1c98d0fe92d640efefab
diff --git a/ninja_writer.go b/ninja_writer.go
index 7ab0e15..883228d 100644
--- a/ninja_writer.go
+++ b/ninja_writer.go
@@ -22,10 +22,13 @@
 )
 
 const (
-	indentWidth = 4
-	lineWidth   = 80
+	indentWidth    = 4
+	maxIndentDepth = 2
+	lineWidth      = 80
 )
 
+var indentString = strings.Repeat(" ", indentWidth*maxIndentDepth)
+
 type ninjaWriter struct {
 	writer io.Writer
 
@@ -108,72 +111,42 @@
 	const lineWrapLen = len(" $")
 	const maxLineLen = lineWidth - lineWrapLen
 
-	line := "build"
-
-	appendWithWrap := func(s string) (err error) {
-		if len(line)+len(s) > maxLineLen {
-			_, err = fmt.Fprintf(n.writer, "%s $\n", line)
-			line = strings.Repeat(" ", indentWidth*2)
-			s = strings.TrimLeftFunc(s, unicode.IsSpace)
-		}
-		line += s
-		return
+	wrapper := ninjaWriterWithWrap{
+		ninjaWriter: n,
+		maxLineLen:  maxLineLen,
 	}
 
+	wrapper.WriteString("build")
+
 	for _, output := range outputs {
-		err := appendWithWrap(" " + output)
-		if err != nil {
-			return err
-		}
+		wrapper.WriteStringWithSpace(output)
 	}
 
-	err := appendWithWrap(":")
-	if err != nil {
-		return err
-	}
+	wrapper.WriteString(":")
 
-	err = appendWithWrap(" " + rule)
-	if err != nil {
-		return err
-	}
+	wrapper.WriteStringWithSpace(rule)
 
 	for _, dep := range explicitDeps {
-		err := appendWithWrap(" " + dep)
-		if err != nil {
-			return err
-		}
+		wrapper.WriteStringWithSpace(dep)
 	}
 
 	if len(implicitDeps) > 0 {
-		err := appendWithWrap(" |")
-		if err != nil {
-			return err
-		}
+		wrapper.WriteStringWithSpace("|")
 
 		for _, dep := range implicitDeps {
-			err := appendWithWrap(" " + dep)
-			if err != nil {
-				return err
-			}
+			wrapper.WriteStringWithSpace(dep)
 		}
 	}
 
 	if len(orderOnlyDeps) > 0 {
-		err := appendWithWrap(" ||")
-		if err != nil {
-			return err
-		}
+		wrapper.WriteStringWithSpace("||")
 
 		for _, dep := range orderOnlyDeps {
-			err := appendWithWrap(" " + dep)
-			if err != nil {
-				return err
-			}
+			wrapper.WriteStringWithSpace(dep)
 		}
 	}
 
-	_, err = fmt.Fprintln(n.writer, line)
-	return err
+	return wrapper.Flush()
 }
 
 func (n *ninjaWriter) Assign(name, value string) error {
@@ -184,8 +157,7 @@
 
 func (n *ninjaWriter) ScopedAssign(name, value string) error {
 	n.justDidBlankLine = false
-	indent := strings.Repeat(" ", indentWidth)
-	_, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indent, name, value)
+	_, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indentString[:indentWidth], name, value)
 	return err
 }
 
@@ -195,27 +167,18 @@
 	const lineWrapLen = len(" $")
 	const maxLineLen = lineWidth - lineWrapLen
 
-	line := "default"
-
-	appendWithWrap := func(s string) (err error) {
-		if len(line)+len(s) > maxLineLen {
-			_, err = fmt.Fprintf(n.writer, "%s $\n", line)
-			line = strings.Repeat(" ", indentWidth*2)
-			s = strings.TrimLeftFunc(s, unicode.IsSpace)
-		}
-		line += s
-		return
+	wrapper := ninjaWriterWithWrap{
+		ninjaWriter: n,
+		maxLineLen:  maxLineLen,
 	}
 
+	wrapper.WriteString("default")
+
 	for _, target := range targets {
-		err := appendWithWrap(" " + target)
-		if err != nil {
-			return err
-		}
+		wrapper.WriteString(" " + target)
 	}
 
-	_, err := fmt.Fprintln(n.writer, line)
-	return err
+	return wrapper.Flush()
 }
 
 func (n *ninjaWriter) BlankLine() (err error) {
@@ -227,32 +190,55 @@
 	return err
 }
 
-func writeAssignments(w io.Writer, indent int, assignments ...string) error {
-	var maxNameLen int
-	for i := 0; i < len(assignments); i += 2 {
-		name := assignments[i]
-		err := validateNinjaName(name)
-		if err != nil {
-			return err
-		}
-		if maxNameLen < len(name) {
-			maxNameLen = len(name)
-		}
+type ninjaWriterWithWrap struct {
+	*ninjaWriter
+	maxLineLen int
+	writtenLen int
+	err        error
+}
+
+func (n *ninjaWriterWithWrap) writeString(s string, space bool) {
+	if n.err != nil {
+		return
 	}
 
-	indentStr := strings.Repeat(" ", indent*indentWidth)
-	extraIndentStr := strings.Repeat(" ", (indent+1)*indentWidth)
-	replacer := strings.NewReplacer("\n", "$\n"+extraIndentStr)
-
-	for i := 0; i < len(assignments); i += 2 {
-		name := assignments[i]
-		value := replacer.Replace(assignments[i+1])
-		_, err := fmt.Fprintf(w, "%s% *s = %s\n", indentStr, maxNameLen, name,
-			value)
-		if err != nil {
-			return err
-		}
+	spaceLen := 0
+	if space {
+		spaceLen = 1
 	}
 
-	return nil
+	if n.writtenLen+len(s)+spaceLen > n.maxLineLen {
+		_, n.err = io.WriteString(n.writer, " $\n")
+		if n.err != nil {
+			return
+		}
+		_, n.err = io.WriteString(n.writer, indentString[:indentWidth*2])
+		if n.err != nil {
+			return
+		}
+		n.writtenLen = indentWidth * 2
+		s = strings.TrimLeftFunc(s, unicode.IsSpace)
+	} else if space {
+		io.WriteString(n.writer, " ")
+		n.writtenLen++
+	}
+
+	_, n.err = io.WriteString(n.writer, s)
+	n.writtenLen += len(s)
+}
+
+func (n *ninjaWriterWithWrap) WriteString(s string) {
+	n.writeString(s, false)
+}
+
+func (n *ninjaWriterWithWrap) WriteStringWithSpace(s string) {
+	n.writeString(s, true)
+}
+
+func (n *ninjaWriterWithWrap) Flush() error {
+	if n.err != nil {
+		return n.err
+	}
+	_, err := io.WriteString(n.writer, "\n")
+	return err
 }